Filipe, 24.09.2026: "wenn ich bei der einen was mache soll nichts bei
der anderen passieren." -- und heute: "ja los".
GEMESSEN, BEVOR ETWAS ANGEFASST WURDE (jedes Brett x beide Adressen x
vier Rollen):
AGENTUR-Adresse DogFather 18 Bretter
Manager 6 Bretter
Creator 6 Bretter
Modi kommt nicht rein (401)
CREW-Adresse DogFather 18 Bretter
Modi 13 Bretter
Manager kommt nicht rein (401)
Creator kommt nicht rein (401)
Die Anmeldung war also dicht. Durch griff genau EINE Rolle: DogFather.
Er wohnt in beiden Haeusern, und die Riegel in sichtbarEintrag()
fragten nach der ROLLE, nicht nach der Adresse. Auf der Crew-Adresse
stand damit das Brett der Agentur samt Inhalt.
Nachgemessen ist der Durchgriff AELTER als der gestrige Eventkarten-
Umbau -- zweimal gemessen, mit und ohne ihn, gleiches Ergebnis.
RIEGEL 0 in sichtbarEintrag(): Wer auf einer Adresse angemeldet ist,
sieht nur Eintraege dieses Hauses. Er haengt den uebrigen Riegeln
UM, statt in jeden Ausgang geschrieben zu werden -- die Funktion hat
drei Rueckgabepunkte, und der naechste waere sonst wieder offen.
Nachher, dieselbe Messung: DogFather sieht auf der Agenturadresse nur
Agentur-Eintraege, auf der Crew-Adresse nur die des Rudels. Manager,
Creator und Modi unveraendert.
WAS DABEI SCHIEFGING UND WIE ES AUFFIEL
1. Die erste Fassung liess bei `haus IS NULL` den BEREICH entscheiden.
pruef-haus-trennung.mjs wurde sofort rot: "Lunas Live vom Montag"
verschwand von der Agenturadresse. `live`, `technik` und
`community` tragen BEIDES -- die Kacheln von Team Dogi und die
Creator-Akten. Eine Regel, die jedem Brett genau ein Haus zuweist,
kann das nicht. Jetzt bleibt ein Eintrag ohne Haus sichtbar: ein
Eintrag zu viel faellt auf, ein fehlender nicht.
2. Damit NULL kein Dauerloch ist: FUENF von ACHT Stellen, die
Eintraege anlegen, setzten `haus` gar nicht (workspace-video.js,
-content.js, -bewerbung.js, -treff.js, -vorlagen.js). Nachgetragen.
3. Und `person.haus` war dafuer der falsche Massstab: Legt DogFather
ueber die Agenturadresse ein Highlight an, gehoert es trotzdem dem
Rudel -- sonst sieht die Community es nie. pruef-treff.mjs hat das
gefunden (2 Fehler). Neu: hausFuerNeuenEintrag() -- bei den sieben
Brettern des Rudels entscheidet das BRETT, sonst die Adresse.
NEU: pruef-haus-luecke.mjs (12 Pruefungen). Sie sucht die
Einfuege-Stellen im Quelltext und wird rot, sobald eine neunte
dazukommt, die `haus` vergisst -- mit Gegenprobe, dass das Suchmuster
eine solche Stelle auch wirklich erkennt. Ein Kommentar daneben haette
es nicht verhindert; das steht so schon im Projektgedaechtnis.
NEBENBEI: In bereich.js stand seit gestern `|| "Agentur-Events"` als
Rueckfall fuer das Etikett der Vorschau. pruef-treff.mjs verbietet
das zu Recht -- wie ein Brett heisst, haengt am Haus, und der Server
sagt es. Der Rueckfall ist weg; fehlt die Angabe, steht lieber kein
Etikett da als ein falsches.
pruef-treff.mjs nachgezogen (85 -> 86 Pruefungen, nicht weniger): Die
Zusage "DogFather sieht den Beitrag" wird jetzt auf der Crew-Adresse
geprueft, mit Gegenprobe fuer die Agenturadresse.
GEPRUEFT, alle gruen:
pruef-haus-luecke 12 pruef-haus-trennung 100
pruef-crew-adresse 169 pruef-treff 86
pruef-eventkarte 77 pruef-agentur 62
pruef-haus-seiten 38 pruef-eintrag-bild 24
pruef-vorlagen 24 pruef-video, -content, -bewerbung,
-bereiche-lesend: in Ordnung
Co-Authored-By: Claude Opus 5 <[email protected]>
3249 lines
150 KiB
JavaScript
3249 lines
150 KiB
JavaScript
/* =====================================================================
|
||
workspace-bereiche.js — LIVE, Content, Technik, Community und Schutz.
|
||
|
||
Diese fuenf Bereiche aus Phase 2 haben im Konzept dieselbe Grundform:
|
||
Eintraege zu einem Creator, mit Art, Datum, Titel, Text und Status.
|
||
Sie unterscheiden sich nur darin, WELCHE Arten es gibt und ob eine
|
||
Bewertung oder eine Dringlichkeit dazugehoert.
|
||
|
||
Deshalb ein gemeinsamer Unterbau statt fuenf fast gleicher Module:
|
||
eine Tabelle, eine Sichtbarkeitsregel, eine Pruefung. Ein Fehler laesst
|
||
sich damit an einer Stelle beheben statt an fuenf, und ein neuer
|
||
Bereich ist ein Eintrag in BEREICHE -- kein neues Modul.
|
||
|
||
Die Feldnamen und Arten stammen woertlich aus dem Deck (Seiten 7-12).
|
||
===================================================================== */
|
||
|
||
import express from "express";
|
||
/* Fuer das Anhaengen von Bildern an einen Beitrag (24.09.2026).
|
||
`dateiErkennen` kommt aus dem Chat -- dort steht die einzige
|
||
Fassung, und an ihr haengt die Sicherheit aller Uploads im Haus. */
|
||
import { writeFileSync, mkdirSync, unlinkSync } from "node:fs";
|
||
import { join } from "node:path";
|
||
import { randomBytes, createHash } from "node:crypto";
|
||
import { dateiErkennen } from "./workspace-chat.js";
|
||
import { TREFF_START, VORSCHLAG_HINWEIS } from "./workspace-treff-start.js";
|
||
import { vorlagenFuer } from "./workspace-treff-vorlagen.js";
|
||
import { ARBEIT_START } from "./workspace-arbeit-start.js";
|
||
/* Wann ein Zettel einen Termin hat -- DIESELBE Regel, aus der auch der
|
||
Kalender seine Auswahl baut. Sie steht in einer eigenen Datei, weil
|
||
sie jetzt zwei Verwender hat: Der Kalender fragt "welche gehoeren
|
||
hinein?", die Brettkarte fragt "stehe ICH drin?". Zwei Fassungen
|
||
derselben Regel laufen auseinander, und dann behauptet die Karte
|
||
etwas, das der Kalender nicht zeigt. */
|
||
import { terminVon } from "./workspace-termin-regel.js";
|
||
/* OB EIN WEG UEBERHAUPT OFFEN STEHT -- gefragt werden die BEIDEN
|
||
Schranken, die es gibt, und zwar hier am Server statt im Browser.
|
||
|
||
Anlass (19.09.2026): Auf jeder Brettkarte mit Termin stand "Im
|
||
Kalender am 24.09." als Link. Fuer die Community fuehrt er ins
|
||
Leere -- `kalender.html` steht in der Rechtetafel auf ALLE, und
|
||
ALLE ist ausdruecklich OHNE "gast". Ein Klick landete wortlos auf
|
||
der Startseite.
|
||
|
||
Die Regel im Browser nachzubauen waere die vierte Stelle, an der
|
||
dieselbe Frage beantwortet wird (Kachel, Rechtetafel, Adressregel
|
||
-- und dann der Link). Die vierte ist immer die, die etwas
|
||
durchlaesst. */
|
||
import { darfSeite } from "./rechte.js";
|
||
|
||
/* EIN KATALOG AUS ZWEI QUELLEN (17.09.2026)
|
||
|
||
Die sieben Bretter des Treffs stehen in workspace-treff-start.js,
|
||
die elf Arbeitsbereiche in workspace-arbeit-start.js. Getrennte
|
||
Dateien, weil die Texte fuer voellig verschiedene Leser geschrieben
|
||
sind -- die einen fuer die Community, die anderen fuer das Team.
|
||
|
||
HIER WERDEN SIE EINMAL ZUSAMMENGEFUEHRT und nicht an jeder der
|
||
beiden Abfragestellen einzeln. Zwei Stellen mit "das eine ODER das
|
||
andere" waeren zwei Stellen, an denen man beim naechsten Katalog
|
||
eine vergisst -- und die vergessene faellt nicht auf, weil sie
|
||
einfach nichts anzeigt.
|
||
|
||
Ueberschneiden duerfen sich die Schluessel nicht; sollte es je
|
||
passieren, gewinnt der Treff (er ist oeffentlich, dort waere ein
|
||
falscher Text teurer). */
|
||
const STARTKATALOG = { ...ARBEIT_START, ...TREFF_START };
|
||
|
||
/* WOHIN DIE BILDER EINES BEITRAGS GEHEN (24.09.2026).
|
||
|
||
DERSELBE ORDNER WIE DIE DATEIABLAGE -- und das ist Absicht, keine
|
||
Bequemlichkeit: Die Auslieferung (/api/dateien/:id/bild) sucht dort
|
||
und kennt nur einen Pfad. Ein eigener Ordner haette eine zweite
|
||
Stelle gebraucht, die ihn kennt, und Coverbilder aus dem
|
||
Video-Einlesen liegen ohnehin schon hier.
|
||
|
||
Beide Namen stehen ueber join(DATEN_ORDNER, ...) -- genau dieses
|
||
Muster liest tools/wiederherstellung-proben.mjs aus, um zu pruefen,
|
||
ob die Sicherung sie mitnimmt. */
|
||
const BILD_ORDNER_EINTRAG = join(DATEN_ORDNER, "dateien");
|
||
const ABLAGE_ORDNER_EINTRAG = join(DATEN_ORDNER, "dateien");
|
||
import {
|
||
db, protokolliere, echteIp, sitzungLesen, DATEN_ORDNER, betreutWo, darfCreator, betreuteIds, istLeitung, istDogFather, siehtAlles, istSpicy, ohneDogFather,
|
||
externPruefen, externSql,
|
||
ohneTeamDogi, ohneAgentur, siehtModis, fuehrtTeamDogi, MODI_BEREICHE_ERLAUBT, brettHinterSeiteOffen, TEAM_DOGI_ROLLEN,
|
||
TREFF_BRETTER, TREFF_ROLLEN, AUSSEN_ROLLEN,
|
||
heuteLokal,
|
||
gehoertAufDieseAdresse,
|
||
bereicheFuer, kanaeleFuer, KANAELE,
|
||
hausFuerNeuenEintrag,
|
||
} from "./workspace.js";
|
||
import { nachrichtSchicken } from "./workspace-chat.js";
|
||
import {
|
||
darfSchreiben, freigabeBedingung, entfernenVorbereiten, TREFF_TEAM_ROLLEN,
|
||
sofortFreigeben,
|
||
TREFF_FREIGABE_BRETTER, mitRollenname,
|
||
} from "./workspace-treff.js";
|
||
/* Wer eine Anfrage stellen darf -- dieselbe Menge, die der
|
||
Bewerbungsweg selbst prueft. Eine zweite hier waere die, die beim
|
||
naechsten Nachschaerfen auseinanderlaeuft. */
|
||
import { BEWERBUNG_ROLLEN } from "./workspace-bewerbung-rollen.js";
|
||
|
||
export const bereicheRouter = express.Router();
|
||
|
||
/* Die Bereiche und ihre Arten. Bewusst hier und nicht in der Datenbank:
|
||
Es sind Festlegungen aus dem Konzept, keine Nutzdaten. */
|
||
/** Wie lange eine Ansage am Anschlagbrett haengt -- in Stunden.
|
||
*
|
||
* Filipe am 22.09.2026: „die sachen sollen in der seite vom
|
||
* anschlagbrett immer automatisch nach 24h von da verschwinden."
|
||
*
|
||
* ALS ZAHL AN EINER STELLE, damit die Anzeige („seit gestern") und
|
||
* die Regel (was noch haengt) nicht auseinanderlaufen koennen. Sie
|
||
* geht mit der Antwort an den Browser -- eine 24 im Skript waere die
|
||
* zweite Fassung, und beim naechsten Mal stuende dort eine andere
|
||
* Zahl als hier. */
|
||
export const ANSCHLAG_STUNDEN = 24;
|
||
|
||
export const BEREICHE = {
|
||
/* =====================================================================
|
||
DIE SIEBEN BRETTER DES TREFFS (11.09.2026)
|
||
|
||
Der Raum fuer die Zuschauer. Sie laufen ueber dieselbe Maschinerie
|
||
wie Ideen-Board und Angebote -- ein Bereich, seine Arten, dieselbe
|
||
Seite. Eine eigene Bauart daneben waere eine zweite
|
||
Sichtbarkeitsregel, und genau deren Vervielfaeltigung hat am
|
||
10.09. ein Leck verursacht.
|
||
|
||
ALLE SIEBEN HABEN `fuerAlle`: Im Treff sieht jeder jeden Beitrag.
|
||
Ein Raum, in dem man nur sieht, was man selbst geschrieben hat,
|
||
waere kein Raum. Die Grenze nach aussen zieht nicht `fuerAlle`,
|
||
sondern der Riegel in sichtbarEintrag() -- die Agentur sieht kein
|
||
einziges dieser Bretter.
|
||
|
||
ALLE SIEBEN HABEN `ohneCreatorBezug`: Hier geht es um niemanden aus
|
||
der Agentur. Das Feld waere nicht nur ueberfluessig, es waere die
|
||
Einladung, versehentlich einen Creator mit einem Zuschauer zu
|
||
verknuepfen.
|
||
===================================================================== */
|
||
|
||
anschlag: {
|
||
name: "Anschlagbrett",
|
||
/* Was dasteht, wenn nichts dasteht. Ein Bereich, der
|
||
schweigt, sieht kaputt aus statt leer. */
|
||
leer: "Gerade gilt nichts Besonderes. Das ist eine gute Nachricht.",
|
||
ober: "Vom Team",
|
||
arten: {
|
||
ansage: "Ansage",
|
||
regel: "Neue Regel",
|
||
hinweis: "Hinweis",
|
||
danke: "Danke an euch",
|
||
},
|
||
bewertung: false,
|
||
dringlichkeit: false,
|
||
ohneCreatorBezug: true,
|
||
fuerAlle: true,
|
||
},
|
||
|
||
ansteht: {
|
||
name: "Was ansteht",
|
||
/* Was dasteht, wenn nichts dasteht. Ein Bereich, der
|
||
schweigt, sieht kaputt aus statt leer. */
|
||
leer: "Noch kein Termin eingetragen. Der nächste Stream steht hier, sobald er feststeht.",
|
||
ober: "Termine",
|
||
/* AUSDRUECKLICH NICHT AN DEN ECHTEN KALENDER GEKOPPELT.
|
||
|
||
Ein Termin traegt Titel, Ort und Gegenueber -- und die Regel im
|
||
Haus lautet seit dem 07.09., dass ein Kalender als Ganzes privat
|
||
ist. Eine Filterung waere eine Einstellung, die man falsch setzen
|
||
kann; ein Eintrag hier ist eine Entscheidung, die jemand trifft.
|
||
Der Preis ist, dass das Team es zweimal schreibt. Das ist der
|
||
guenstigere von beiden. */
|
||
arten: {
|
||
stream: "Stream",
|
||
event: "Event",
|
||
turnier: "Turnier",
|
||
pause: "Keine Sendung",
|
||
},
|
||
bewertung: false,
|
||
dringlichkeit: false,
|
||
ohneCreatorBezug: true,
|
||
fuerAlle: true,
|
||
},
|
||
|
||
treff: {
|
||
name: "Das Rudel",
|
||
/* Was dasteht, wenn nichts dasteht. Ein Bereich, der
|
||
schweigt, sieht kaputt aus statt leer. */
|
||
leer: "Noch hat niemand Hallo gesagt. Sei der Erste — ein Satz reicht.",
|
||
ober: "Euer Raum",
|
||
/* DIE REIHENFOLGE IST ABSICHT. Das erste Feld bestimmt, was
|
||
geschrieben wird -- wer ein Formular oeffnet, dessen erste
|
||
Auswahl "Problem" heisst, schreibt Probleme auf.
|
||
|
||
"Hallo, ich bin neu" steht vorn, weil die ersten sieben Tage
|
||
darueber entscheiden, ob jemand bleibt: Wer in dieser Zeit
|
||
erlebt, dass er hier einen Platz hat, kommt deutlich haeufiger
|
||
wieder. Jede Vorstellung bekommt eine Antwort -- das ist der
|
||
ganze Trick, und er steht in keinem Code. */
|
||
arten: {
|
||
hallo: "Hallo, ich bin neu",
|
||
frage: "Frage",
|
||
lob: "Das war gut",
|
||
problem: "Etwas stimmt nicht",
|
||
},
|
||
bewertung: false,
|
||
dringlichkeit: false,
|
||
ohneCreatorBezug: true,
|
||
fuerAlle: true,
|
||
},
|
||
|
||
wunsch: {
|
||
name: "Wunschliste",
|
||
/* Was dasteht, wenn nichts dasteht. Ein Bereich, der
|
||
schweigt, sieht kaputt aus statt leer. */
|
||
leer: "Noch kein Wunsch. Was würdest du im nächsten Stream gern sehen?",
|
||
ober: "Was ihr euch wünscht",
|
||
/* DER WEG EINES WUNSCHES (17.09.2026).
|
||
|
||
Bis heute kannte ein Eintrag zwei Zustaende: offen und erledigt.
|
||
Fuer eine Wunschliste ist das zu wenig -- und der Mangel ist
|
||
kein Schoenheitsfehler, sondern der Grund, warum solche Listen
|
||
sterben:
|
||
|
||
Eine Wunschliste ohne sichtbare Erfuellung ist ein Briefkasten
|
||
ohne Postbote. Nach dem dritten unbeantworteten Wunsch
|
||
schreibt niemand mehr.
|
||
|
||
"Diesmal nicht" gehoert ausdruecklich dazu. Ein Nein ist eine
|
||
Antwort; Schweigen ist keine. Wer sieht, dass entschieden wurde,
|
||
schreibt beim naechsten Mal wieder -- auch wenn die Entscheidung
|
||
gegen ihn ausfiel.
|
||
|
||
DIE DATENBANK KONNTE DAS SCHON. Ihre CHECK-Regel erlaubt seit
|
||
laengerem 'angenommen' und 'abgelehnt'; nur die Pruefung im
|
||
Server liess sie nicht durch. Die Tabelle war der Logik voraus. */
|
||
zustaende: {
|
||
offen: "Offen",
|
||
angenommen: "Wird gemacht",
|
||
erledigt: "Gemacht",
|
||
abgelehnt: "Diesmal nicht",
|
||
},
|
||
/* Dazu ein Datum: "Wird gemacht" allein ist ein Versprechen,
|
||
"Wird gemacht -- am 24.09." ist ein Termin. */
|
||
mitGeplant: true,
|
||
arten: {
|
||
stream: "Für den Stream",
|
||
technik: "Technik",
|
||
gemeinsam: "Für die Community",
|
||
sonstiges: "Sonstiges",
|
||
},
|
||
bewertung: false,
|
||
/* KEINE DRINGLICHKEIT VON EINEM EINZELNEN. Was wichtig ist,
|
||
entscheidet hier nicht der Schreiber, sondern wie viele
|
||
mitwollen -- "Will ich auch" kommt als naechster Schritt. Eine
|
||
Dringlichkeit daneben waere eine zweite Rangordnung, und die
|
||
beiden wuerden einander widersprechen. */
|
||
dringlichkeit: false,
|
||
ohneCreatorBezug: true,
|
||
fuerAlle: true,
|
||
},
|
||
|
||
highlight: {
|
||
name: "Highlights",
|
||
/* Was dasteht, wenn nichts dasteht. Ein Bereich, der
|
||
schweigt, sieht kaputt aus statt leer. */
|
||
leer: "Hier kommen eure Clips und Bilder hin. Der erste darf ruhig wackelig sein.",
|
||
ober: "Von euch",
|
||
arten: {
|
||
clip: "Clip",
|
||
bild: "Bild",
|
||
fanart: "Fanart",
|
||
moment: "Moment",
|
||
},
|
||
bewertung: false,
|
||
dringlichkeit: false,
|
||
ohneCreatorBezug: true,
|
||
fuerAlle: true,
|
||
},
|
||
|
||
regeln: {
|
||
name: "Regeln & Hilfe",
|
||
/* Was dasteht, wenn nichts dasteht. Ein Bereich, der
|
||
schweigt, sieht kaputt aus statt leer. */
|
||
leer: "Hier stehen die Regeln des Rudels — sobald sie eingetragen sind.",
|
||
ober: "Wie es hier läuft",
|
||
/* REGELN IN EINFACHER SPRACHE UND AN ECHTEN FAELLEN. Vage Regeln
|
||
erzeugen Grenzfaelle, und die muss dann jeder Modi einzeln
|
||
entscheiden -- das ist der Weg, auf dem ein Team uneinheitlich
|
||
wird und die Community es merkt. */
|
||
arten: {
|
||
regel: "Regel",
|
||
frage: "Häufige Frage",
|
||
hilfe: "Hilfe",
|
||
folge: "Was passiert, wenn",
|
||
},
|
||
bewertung: false,
|
||
dringlichkeit: false,
|
||
ohneCreatorBezug: true,
|
||
fuerAlle: true,
|
||
},
|
||
|
||
mitmachen: {
|
||
name: "Mitmachen",
|
||
/* Was dasteht, wenn nichts dasteht. Ein Bereich, der
|
||
schweigt, sieht kaputt aus statt leer. */
|
||
leer: "Hier steht, wie man dazugehören kann — sobald es eingetragen ist.",
|
||
ober: "Nachwuchs",
|
||
/* DIE BRUECKE ZU DEN TALENTEN. Wer sich hier meldet, ist ein
|
||
Kandidat -- und Kandidaten gehoeren in die Kategorie, die es
|
||
dafuer schon gibt, nicht in eine zweite Liste daneben.
|
||
|
||
Daneben gehoert der ehrliche Satz, dass eine Bewerbung nicht der
|
||
Hauptweg ist: Der treffsicherste Weg ist, dass jemandem aus dem
|
||
Team auffaellt, wie du dich verhaeltst. Das steht so im Brett,
|
||
weil es stimmt -- und weil es die richtige Erwartung setzt. */
|
||
arten: {
|
||
gesucht: "Wir suchen",
|
||
lust: "Ich hätte Lust",
|
||
frage: "Frage dazu",
|
||
},
|
||
bewertung: false,
|
||
dringlichkeit: false,
|
||
ohneCreatorBezug: true,
|
||
fuerAlle: true,
|
||
},
|
||
|
||
live: {
|
||
/* HEISST WIE DIE KACHEL (20.09.2026). Die Kachel sagt
|
||
„Live-Ablauf", die Seite sagte „LIVE-Analyse" -- und der
|
||
Untertext der Kachel verspricht eine Checkliste. Wer darauf
|
||
tippt, landet unter einem anderen Namen. Der Weg faengt auf
|
||
der Startseite an, also gewinnt der Name von dort. */
|
||
name: "Live-Ablauf",
|
||
/* Was dasteht, wenn nichts dasteht. Ein Bereich, der
|
||
schweigt, sieht kaputt aus statt leer -- und der Satz
|
||
sagt NICHT nur "leer", sondern was hier hingehoert. */
|
||
leer: "Noch keine Notiz zu einem Stream. Hier landet, was vorher vorbereitet, während der Sendung mitgeschrieben und danach ausgewertet wird.",
|
||
arten: { vorbereitung: "Vorbereitung", mitschrift: "Während LIVE", auswertung: "Auswertung" },
|
||
bewertung: true, // Review-Score aus dem Konzept, Seite 7
|
||
dringlichkeit: false,
|
||
},
|
||
/* Content ist der einzige Bereich mit einer STRECKE statt einer Liste:
|
||
Die Arten sind hier Stufen, die ein Video der Reihe nach durchlaeuft.
|
||
Grundlage ist der Satz aus dem Konzept ("Hook bis Upload") und die
|
||
Recherche vom 31.08.2026: Eine Redaktionsplanung ist eine Strecke,
|
||
kein Kalender -- ein Kalender voller Termine verbirgt genau die drei
|
||
Stellen, an denen es klemmt.
|
||
|
||
Fuenf Stufen, nicht drei: Zwischen "Idee" und "Veroeffentlicht" lag
|
||
vorher alles, was den Unterschied macht. Und nicht mehr als fuenf --
|
||
jede weitere Spalte ist eine, die auf dem Handy nicht mehr passt. */
|
||
content: {
|
||
/* Umbenannt am 01.09.2026: "Planung" klang nach Terminen und
|
||
Tabellen. Was hier wirklich passiert, ist das Sammeln und
|
||
Weiterentwickeln von IDEEN -- der Kalender daneben plant. */
|
||
name: "Content-Ideen",
|
||
/* Was dasteht, wenn nichts dasteht. Ein Bereich, der
|
||
schweigt, sieht kaputt aus statt leer -- und der Satz
|
||
sagt NICHT nur "leer", sondern was hier hingehoert. */
|
||
leer: "Noch keine Idee gesammelt. Fang klein an — ein Stichwort reicht, der Rest kommt beim Drehen.",
|
||
arten: {
|
||
idee: "Idee",
|
||
skript: "Hook & Skript",
|
||
gedreht: "Gedreht",
|
||
fertig: "Fertig & geplant",
|
||
veroeffentlicht: "Veröffentlicht",
|
||
},
|
||
bewertung: false,
|
||
dringlichkeit: false,
|
||
strecke: true, // hat eine Reihenfolge, nicht nur Kategorien
|
||
},
|
||
technik: {
|
||
name: "Technik",
|
||
/* Was dasteht, wenn nichts dasteht. Ein Bereich, der
|
||
schweigt, sieht kaputt aus statt leer -- und der Satz
|
||
sagt NICHT nur "leer", sondern was hier hingehoert. */
|
||
leer: "Noch kein Eintrag. Hierhin gehört, wie das Setup aufgebaut ist, was gerade klemmt und wie es beim letzten Mal gelöst wurde.",
|
||
arten: { setup: "Setup", problem: "Problem", loesung: "Lösung", anleitung: "Anleitung" },
|
||
bewertung: false,
|
||
dringlichkeit: true,
|
||
},
|
||
community: {
|
||
name: "Community",
|
||
/* Was dasteht, wenn nichts dasteht. Ein Bereich, der
|
||
schweigt, sieht kaputt aus statt leer -- und der Satz
|
||
sagt NICHT nur "leer", sondern was hier hingehoert. */
|
||
leer: "Noch nichts eingetragen. Hier steht, was in der Community läuft — Aktionen, Moderation und was beim letzten Konflikt geholfen hat.",
|
||
arten: { moderation: "Moderation", aktion: "Aktion", konflikt: "Konflikt" },
|
||
bewertung: false,
|
||
dringlichkeit: true,
|
||
},
|
||
schutz: {
|
||
name: "Schutz & Regeln",
|
||
/* Was dasteht, wenn nichts dasteht. Ein Bereich, der
|
||
schweigt, sieht kaputt aus statt leer -- und der Satz
|
||
sagt NICHT nur "leer", sondern was hier hingehoert. */
|
||
leer: "Noch kein Eintrag. Hierhin gehören die Regeln, Vorfälle und was daraus gelernt wurde — damit beim nächsten Mal niemand raten muss.",
|
||
arten: { richtlinie: "Richtlinie", vorfall: "Vorfall", eskalation: "Eskalation", gelernt: "Gelernt" },
|
||
bewertung: false,
|
||
dringlichkeit: true,
|
||
},
|
||
/* DER SECHSTE BEREICH (06.09.2026).
|
||
|
||
Kernprinzip 04 des Konzepts: "Die Agentur bleibt angebunden." Fuenf
|
||
Bereiche standen, dieser fehlte -- und damit fehlte der einzige
|
||
Ort, an dem steht, welche Kampagne laeuft, welche Schulung ansteht
|
||
und wie ein Anliegen an die Agentur ausgegangen ist. Das lief
|
||
ueber private Nachrichten: nicht auffindbar, nicht nachvollziehbar,
|
||
und beim naechsten Mal fing man wieder von vorn an.
|
||
|
||
VIER ARTEN, und die vierte ist der Punkt:
|
||
kampagne was laeuft, bis wann, unter welcher Bedingung
|
||
schulung Termine und Inhalte, verweist auf die Bibliothek
|
||
anliegen der OFFIZIELLE Weg hin -- mit Stand
|
||
zustaendig wer wofuer zustaendig ist: wir oder die Agentur
|
||
|
||
Die letzte Art ist keine Verlegenheit, sondern das, was das Konzept
|
||
ausdruecklich fordert ("Betreuung & Umsetzung ist unsere Seite,
|
||
offizielle Wege und Plattformthemen sind Agenturseite"). Solange
|
||
das nur im Kopf steht, wird bei jedem Streitfall neu verhandelt,
|
||
wer eigentlich haette handeln muessen.
|
||
|
||
Dringlichkeit: ja. Ein Anliegen an die Agentur, das drei Wochen
|
||
liegt, ist etwas anderes als eines von gestern -- und ohne Stufe
|
||
sieht man den Unterschied in einer Liste nicht.
|
||
Bewertung: nein. Hier wird nichts benotet; es ist ein
|
||
Zusammenarbeits-Bereich, kein Analysebereich. */
|
||
/* FUER ALLE, NICHT FUER EINEN (07.09.2026).
|
||
|
||
Die anderen fuenf Bereiche sind Betreuungsakten: Dort steht etwas
|
||
UEBER einen Creator, und nur er und seine Betreuung sehen es. Die
|
||
Agentur ist das Gegenteil -- ein Event, eine Schulung, eine
|
||
Zustaendigkeit gilt fuer alle gleichzeitig.
|
||
|
||
Bis heute lief sie trotzdem unter der Akten-Regel, und das hatte
|
||
eine Folge, die niemandem auffiel: Ein Event ohne Creator-Zuordnung
|
||
traf weder `creator_id = ich` noch `erstellt_von = ich` -- fuer
|
||
jeden Creator war die Agentur-Seite schlicht LEER. Nicht "kaputt",
|
||
nicht "Fehler": leer, so wie eine Seite aussieht, auf der noch
|
||
nichts steht. Gemessen am 07.09.2026: Von drei Eintraegen sah die
|
||
Creatorin genau den einen, der ihr zugeordnet war.
|
||
|
||
Zwei Schalter, weil es zwei verschiedene Fragen sind:
|
||
fuerAlle -- jeder sieht jeden Eintrag dieses Bereichs
|
||
ohneCreatorBezug -- ein Eintrag gehoert hier NIEMANDEM einzeln */
|
||
/* =====================================================================
|
||
DAS IDEEN-BOARD (10.09.2026, Kapitel 5.6 des Anforderungsdokuments)
|
||
|
||
"Sammelstelle fuer Content-, Live- und Community-Ideen mit
|
||
Priorisierung." Genau das steht hier -- und fast nichts davon
|
||
musste neu gebaut werden: `eintraege` hat Titel, Text, Status und
|
||
`dringlichkeit` (hoch/mittel/niedrig), und das IST die
|
||
Priorisierung. Eine eigene Tabelle daneben waere eine VIERTE
|
||
Sichtbarkeitsregel gewesen; genau deren Vervielfaeltigung hat heute
|
||
schon ein Leck verursacht.
|
||
|
||
`ohneCreatorBezug`: Eine Idee gehoert der Runde, nicht einer
|
||
Person. Stuende ein Name daneben, laese sich der Eintrag wie ein
|
||
Auftrag an diese Person -- und niemand faende ihn spaeter wieder,
|
||
wenn sie nicht mehr da ist.
|
||
|
||
KEIN `fuerAlle`. Das waere der naheliegende Griff gewesen (die
|
||
Agentur-Ablage darueber macht es so) und der falsche: `fuerAlle`
|
||
heisst woertlich JEDER, auch Manager, Scouts und Creator. Wer die
|
||
Sammlung sehen darf, entscheidet stattdessen dieselbe Regel wie
|
||
ueberall -- sichtbar() haengt die Modi-Bedingung an, und der Zugang
|
||
zur Seite haengt an siehtModis(). Zwei Wege, eine Antwort.
|
||
|
||
DIE ARTEN SIND DIE DREI AUS DEM DOKUMENT, dazu "Sonstiges": Eine
|
||
Idee, die in keine der drei passt, soll nicht ungeschrieben
|
||
bleiben, weil das Formular sie nicht kennt. */
|
||
/* =====================================================================
|
||
DIE RUECKMELDUNG (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 nicht nur dazu dienen, Leistungen zu kontrollieren. Es
|
||
soll vor allem dabei helfen, als Team besser zu werden."
|
||
|
||
EIN BEREICH FUER BEIDE KAPITEL, NICHT ZWEI. Kapitel 5 (gegenseitiges
|
||
Feedback) und Kapitel 6 (gemeinsame Reflexion) stellen dieselben
|
||
Fragen -- "Was laeuft gut? Was laeuft schlecht? Was fehlt?" --, nur
|
||
einmal an eine Person und einmal an das Team. Zwei Bretter dafuer
|
||
haetten bedeutet, dass man beim Schreiben zuerst entscheiden muss,
|
||
an WEN es geht, bevor man weiss, WAS man sagen will. Hier ist es
|
||
umgekehrt: erst die Sache, dann die Richtung (siehe nur_leitung).
|
||
|
||
DIE NEUN FRAGEN SIND SEINE, wortwoertlich aus dem Pflichtenheft
|
||
zusammengezogen. Sie stehen als "Arten" da und nicht als freies
|
||
Textfeld, weil genau das der Unterschied zwischen einer Sammlung
|
||
und einem Haufen ist: Neun Faecher kann man auswerten, tausend
|
||
Formulierungen nicht.
|
||
|
||
KEINE ANONYMITAET, und das ist eine Entscheidung, keine Luecke. In
|
||
einem Team dieser Groesse waere sie ohnehin keine -- an drei
|
||
Saetzen erkennt jeder jeden. Ein Versprechen, das nicht haelt, ist
|
||
schlimmer als keines. Was es stattdessen gibt, ist die Wahl
|
||
zwischen "fuers Team" und "nur an DogFather". */
|
||
/* =====================================================================
|
||
ENTWICKLUNG (11.09.2026)
|
||
|
||
Filipe: "wo wir aufgaben oder bewertungen ueber modis eingeben
|
||
koennen."
|
||
|
||
ES IST KEINE NOTE, UND DAS IST KEINE ZIMPERLICHKEIT, SONDERN DAS
|
||
ERGEBNIS DER RECHERCHE. Die groesste Untersuchung dazu (Schoepke-
|
||
Gonzalez u. a., New Media & Society 2024, ueber 500 befragte
|
||
Freiwillige) nennt zwei Hauptgruende, warum Moderatoren aufhoeren:
|
||
zu wenig Zeit -- und Konflikte im Team beziehungsweise schaedliches
|
||
Verhalten der Leitung. Eine Bewertungsfunktion ist damit genau das
|
||
Werkzeug, mit dem man ein Team verliert, wenn sie als Ueberwachung
|
||
gebaut ist.
|
||
|
||
Was HAELT, ist ebenso gut belegt: Anerkennung. Dank und
|
||
Rueckmeldung erhoehen die Verweildauer messbar; Rollenklarheit
|
||
senkt Burnout.
|
||
|
||
DARAUS FOLGEN DIE FUENF ARTEN, und ihre Reihenfolge ist Absicht:
|
||
Das Erste, was man sieht, ist "Das laeuft gut". Wer ein Formular
|
||
oeffnet, dessen erstes Feld "Problem" heisst, schreibt Probleme
|
||
auf.
|
||
|
||
"ZU VIEL GERADE" IST DIE WICHTIGSTE ART und die, die es sonst
|
||
nirgends gibt. Der haeufigste Grund zu gehen ist Zeitmangel, und
|
||
der zeigt sich frueh -- nur schreibt ihn niemand auf, weil es
|
||
kein Feld dafuer gibt. Jetzt gibt es eins.
|
||
|
||
DIE PERSON SIEHT DEN BEREICH NICHT. Eine halb sichtbare Akte ist
|
||
schlimmer als eine geschlossene: Niemand weiss mehr, was der
|
||
andere gerade liest. Damit Anerkennung trotzdem ankommt, laesst
|
||
sich jeder Eintrag als Nachricht an die Person schicken -- ein
|
||
ausdruecklicher Schritt, und im Eintrag steht danach, wann er
|
||
getan wurde.
|
||
===================================================================== */
|
||
entwicklung: {
|
||
name: "Entwicklung",
|
||
/* Was dasteht, wenn nichts dasteht. Ein Bereich, der
|
||
schweigt, sieht kaputt aus statt leer -- und der Satz
|
||
sagt NICHT nur "leer", sondern was hier hingehoert. */
|
||
leer: "Noch keine Beobachtung eingetragen. Hier wird festgehalten, was jemand gut kann, woran er arbeitet und was verabredet ist.",
|
||
ober: "Über die Zeit",
|
||
arten: {
|
||
staerke: "Das läuft gut",
|
||
dank: "Danke dafür",
|
||
wachsen: "Daran arbeiten wir",
|
||
last: "Zu viel gerade",
|
||
absprache: "So haben wir es vereinbart",
|
||
},
|
||
bewertung: false,
|
||
dringlichkeit: true,
|
||
dringlichkeitName: "Wie dringend",
|
||
/* Der Personenbezug ist hier der SINN des Eintrags und nicht eine
|
||
Nebenangabe -- deshalb ausdruecklich KEIN `ohneCreatorBezug`. */
|
||
einsatzFeld: true,
|
||
einsatzName: "Was daraus folgen soll",
|
||
},
|
||
|
||
/* =====================================================================
|
||
TALENTE (11.09.2026)
|
||
|
||
Filipe: "auch fuer zuschauer die vielleicht modis werden koennten
|
||
waere auch geil."
|
||
|
||
DIE VIER MERKMALE SIND NICHT AUSGEDACHT. Sie sind das, was sich in
|
||
der Literatur zu Moderatorenauswahl immer wieder findet (ModSquad,
|
||
Kitfox Games, Discord-Leitfaeden): ruhig bleiben, wenn es hitzig
|
||
wird; von selbst helfen; die Regeln UND die Leute kennen; und
|
||
regelmaessig da sein. Ausdruecklich NICHT dabei: "schreibt viel"
|
||
-- angenehm im Chat zu sein ist nachweislich etwas anderes, als
|
||
ein guter Moderator zu sein.
|
||
|
||
Und der treffsicherste Weg ueberhaupt ist der fuenfte: die
|
||
EMPFEHLUNG aus dem Team. Wer schon dabei ist, hat den Menschen im
|
||
Alltag erlebt, nicht in einem Bewerbungsformular.
|
||
|
||
DER STATUS IST DIE PROBEZEIT. `offen` heisst beobachtet,
|
||
`angenommen` heisst angesprochen -- und was daraus wird,
|
||
entscheidet sich in "Personen & Zugaenge" mit einem Zugang auf
|
||
Stufe "Probe". Eine eigene Kandidatentabelle waere ein zweiter Ort
|
||
fuer dieselbe Frage.
|
||
===================================================================== */
|
||
talente: {
|
||
name: "Talente",
|
||
/* Was dasteht, wenn nichts dasteht. Ein Bereich, der
|
||
schweigt, sieht kaputt aus statt leer -- und der Satz
|
||
sagt NICHT nur "leer", sondern was hier hingehoert. */
|
||
leer: "Noch kein Name notiert. Wem im Chat etwas auffällt — jemand, der ruhig bleibt, wenn es hitzig wird, oder der anderen hilft — trägt ihn hier ein.",
|
||
ober: "Nachwuchs",
|
||
arten: {
|
||
ruhig: "Bleibt ruhig, wenn es hitzig wird",
|
||
hilft: "Hilft anderen von selbst",
|
||
kennt: "Kennt die Regeln und die Leute",
|
||
da: "Ist regelmäßig da",
|
||
empfohlen: "Aus dem Team empfohlen",
|
||
},
|
||
bewertung: true,
|
||
bewertungName: "Wie sicher bin ich",
|
||
dringlichkeit: true,
|
||
dringlichkeitName: "Wie eilig",
|
||
ohneCreatorBezug: true,
|
||
einsatzFeld: true,
|
||
einsatzName: "Wo ich ihn oder sie gesehen habe",
|
||
},
|
||
|
||
rueckmeldung: {
|
||
name: "Rückmeldung",
|
||
/* Was dasteht, wenn nichts dasteht. Ein Bereich, der
|
||
schweigt, sieht kaputt aus statt leer -- und der Satz
|
||
sagt NICHT nur "leer", sondern was hier hingehoert. */
|
||
leer: "Noch keine Rückmeldung. Hier kann jeder sagen, was gut läuft und wo es hakt — auch über DogFather und über die Regeln.",
|
||
/* Wie beim Ideen-Board: Das hier ist keine Akte ueber jemanden,
|
||
sondern etwas, das allen gehoert. */
|
||
ober: "Gemeinsam besser werden",
|
||
arten: {
|
||
gut: "Läuft gut",
|
||
haken: "Läuft nicht gut",
|
||
behalten: "Unbedingt behalten",
|
||
dogfather: "An DogFather",
|
||
team: "Was dem Team fehlt",
|
||
regeln: "Regel ändern",
|
||
ablauf: "Besser organisieren",
|
||
idee: "Idee",
|
||
zukunft: "Wunsch für später",
|
||
},
|
||
bewertung: false,
|
||
/* Dringlichkeit ja: "Der Ton war gestern kaputt" ist etwas anderes
|
||
als "waere schoen, irgendwann". Eine Sammlung ohne Rangfolge
|
||
wird nach vier Wochen nicht mehr gelesen. */
|
||
dringlichkeit: true,
|
||
ohneCreatorBezug: true,
|
||
/* Schaltet das Feld "nur an DogFather" frei -- siehe pruefe() und
|
||
sichtbarEintrag(). Ohne dieses Merkmal wird es in jedem anderen
|
||
Bereich stillschweigend verworfen, wie Bewertung und
|
||
Dringlichkeit auch. */
|
||
vertraulich: true,
|
||
},
|
||
|
||
ideen: {
|
||
name: "Ideen-Board",
|
||
/* Was dasteht, wenn nichts dasteht. Ein Bereich, der
|
||
schweigt, sieht kaputt aus statt leer -- und der Satz
|
||
sagt NICHT nur "leer", sondern was hier hingehoert. */
|
||
leer: "Noch keine Idee gesammelt. Alles darf hier rein, auch halb gedacht — sortiert wird später.",
|
||
/* DAS WORT UEBER DEM TITEL. Ueberall sonst steht dort "Betreuung",
|
||
weil die Bereichsseiten Betreuungsakten sind -- ueber jemanden
|
||
gefuehrt, von seinen Betreuern. Eine Ideensammlung ist das
|
||
Gegenteil: Sie gehoert niemandem und wird von allen gefuellt.
|
||
|
||
Es stand fest in bereich.html und wurde nie gesetzt; aufgefallen
|
||
ist es auf dem Bildschirmfoto. Das Feld ist ab jetzt Teil der
|
||
Bereichsangaben, damit der naechste Bereich es einfach mitbringen
|
||
kann statt es zu erben. */
|
||
ober: "Sammelstelle",
|
||
arten: {
|
||
content: "Content-Idee",
|
||
live: "Live-Idee",
|
||
community: "Community-Idee",
|
||
sonstiges: "Sonstiges",
|
||
},
|
||
bewertung: false,
|
||
dringlichkeit: true,
|
||
ohneCreatorBezug: true,
|
||
},
|
||
|
||
/* =====================================================================
|
||
DIE ANGEBOTE (10.09.2026, Wunsch Filipe)
|
||
|
||
*"ich brauch auch noch kategorien wie, angebote, wo die modis
|
||
sachen planen und auch entscheiden was ich machen muss wenn das und
|
||
das erledigt wird."*
|
||
|
||
Ein Modi schlaegt etwas vor -- eine Aktion, ein Format, eine Regel
|
||
-- und schreibt dazu, was DogFather dafuer tun muesste. DogFather
|
||
oder VanVan nimmt an oder lehnt ab; bei Annahme entstehen die
|
||
Aufgaben. Entschieden wird im Eingang, an derselben Stelle
|
||
wie die uebrigen Rueckmeldungen: EIN Eingang, nicht zwei.
|
||
|
||
DIE ZWEI ZAHLEN SIND UMBENANNT, nicht neu erfunden: `bewertung`
|
||
(1-5) heisst hier NUTZEN, `dringlichkeit` heisst AUFWAND. Beide
|
||
Felder gibt es laengst; sie mit neuen Namen zu belegen ist besser,
|
||
als zwei weitere Spalten anzulegen, die dasselbe koennen. Damit
|
||
die Oberflaeche nicht "Dringlichkeit" ueber den Aufwand schreibt,
|
||
stehen die Namen hier.
|
||
|
||
KEIN `fuerAlle`, wie beim Ideen-Board: Wer die Angebote sieht,
|
||
entscheidet dieselbe Regel wie ueberall. */
|
||
angebote: {
|
||
name: "Angebote",
|
||
/* Was dasteht, wenn nichts dasteht. Ein Bereich, der
|
||
schweigt, sieht kaputt aus statt leer -- und der Satz
|
||
sagt NICHT nur "leer", sondern was hier hingehoert. */
|
||
leer: "Noch kein Vorschlag. Wer eine Aktion, ein Format oder eine Regel vorschlagen will, schreibt es hier auf.",
|
||
arten: {
|
||
aktion: "Aktion",
|
||
format: "Format",
|
||
regel: "Regel",
|
||
technik: "Technik",
|
||
sonstiges: "Sonstiges",
|
||
},
|
||
ober: "Vorschläge",
|
||
bewertung: true,
|
||
bewertungName: "Nutzen",
|
||
dringlichkeit: true,
|
||
dringlichkeitName: "Aufwand",
|
||
einsatzFeld: true,
|
||
einsatzName: "Was DogFather dafür tun müsste",
|
||
ohneCreatorBezug: true,
|
||
},
|
||
|
||
agentur: {
|
||
name: "Agentur",
|
||
/* Was dasteht, wenn nichts dasteht. Ein Bereich, der
|
||
schweigt, sieht kaputt aus statt leer -- und der Satz
|
||
sagt NICHT nur "leer", sondern was hier hingehoert. */
|
||
leer: "Noch kein Eintrag. Hier laufen Kampagnen, Schulungen und Anliegen zusammen, die zwischen Team und Agentur besprochen werden.",
|
||
arten: {
|
||
kampagne: "Agentur-Events",
|
||
schulung: "Schulung",
|
||
anliegen: "Anliegen an die Agentur",
|
||
zustaendig: "Zuständigkeit",
|
||
},
|
||
bewertung: false,
|
||
dringlichkeit: true,
|
||
fuerAlle: true,
|
||
ohneCreatorBezug: true,
|
||
},
|
||
};
|
||
|
||
/* Aus den Bereichseinstellungen abgeleitet, nicht danebengeschrieben.
|
||
Eine zweite Liste waere beim naechsten Bereich auseinandergelaufen --
|
||
und zwar still. */
|
||
const BEREICHE_FUER_ALLE = Object.entries(BEREICHE)
|
||
.filter(([, e]) => e.fuerAlle).map(([k]) => k);
|
||
|
||
const DRINGLICHKEITEN = ["hoch", "mittel", "niedrig"];
|
||
const STATUS = ["offen", "erledigt"];
|
||
/* Wie viele Beitraege in einer Stunde? Zehn ist reichlich fuer einen
|
||
Menschen und wenig fuer ein Skript. Die Zahl steht hier und nicht in
|
||
der Route, damit sie an genau einer Stelle steht -- die Pruefung
|
||
liest sie mit. */
|
||
export const BEITRAEGE_JE_STUNDE = 10;
|
||
/* Gilt die Bremse hier? Nur in den Community-Bereichen. Eine
|
||
Content-Planung ist Arbeit am eigenen Kanal; dort waere eine Bremse
|
||
eine Behinderung ohne Gegenwert. */
|
||
const treffBereich = (b) => TREFF_BRETTER.includes(b);
|
||
/* WO EIN GESPRAECH GEMEINT IST (17.09.2026), Stufe 6.
|
||
|
||
Nicht ueberall. Aus dem Plan, §9: "Keine Kommentarfunktion ueberall.
|
||
Nur dort, wo ein Gespraech gemeint ist." Ein Antwortfeld unter jedem
|
||
Aushang hiesse: Jeder Aushang muss betreut werden -- und ein
|
||
unbeantwortetes Antwortfeld ist schlimmer als keins. */
|
||
const MIT_ANTWORTEN = new Set(["treff", "wunsch"]);
|
||
const TITEL_MAX = 160;
|
||
const TEXT_MAX = 6000;
|
||
|
||
/* =====================================================================
|
||
DIE PUNKTE EINES AGENTUR-EVENTS (03.10.2026)
|
||
|
||
`event_aufgaben` und `event_regeln` sind Fliesstext, den DogFather
|
||
tippt -- eine Zeile je Punkt, meistens mit einem Aufzaehlungszeichen
|
||
davor. Dass er von Hand gesetzt wird, sieht man dem Ergebnis an: Im
|
||
echten Oktober-Event stand „·Jeden Tag Live gehen" neben
|
||
„· 33k Diamanten erreichen", einmal mit Leerzeichen, einmal ohne.
|
||
|
||
HIER WIRD DARAUS EINE LISTE. Die Oberflaeche bekommt Punkte, keinen
|
||
Absatz -- damit sie Haken daran haengen kann und die Einrueckung
|
||
nicht mehr davon abhaengt, wie jemand getippt hat.
|
||
|
||
DER SPEICHERTEXT BLEIBT UNVERAENDERT. Zerlegt wird beim LESEN, nicht
|
||
beim Schreiben: Wer das Event spaeter bearbeitet, soll genau das
|
||
wiederfinden, was er geschrieben hat. Und ein Textfeld, das beim
|
||
Speichern umformatiert wird, ist ein Textfeld, dem man nicht traut.
|
||
===================================================================== */
|
||
|
||
/* Die Zeichen, mit denen Menschen Aufzaehlungen anfangen. Sie gehoeren
|
||
nicht zum Inhalt des Punktes -- und wer heute „·" tippt und morgen
|
||
„-", meint denselben Punkt. */
|
||
const PUNKT_ZEICHEN = /^[\s]*[-–—•·*▪◦‣>+]+[\s]*/u;
|
||
|
||
/** Der Kern einer Aufgabenzeile, ohne Zierde.
|
||
* Nur fuer den Vergleich gedacht -- angezeigt wird der Originaltext. */
|
||
function punktKern(zeile) {
|
||
return String(zeile || "")
|
||
.replace(PUNKT_ZEICHEN, "")
|
||
/* Eine fuehrende Nummerierung („1.", „2)") ebenfalls weg: Wer
|
||
einen Punkt davorsetzt, verschiebt sonst alle Nummern und damit
|
||
alle Schluessel -- genau der stille Haken-Umzug, den die Tabelle
|
||
verhindern soll. */
|
||
.replace(/^\s*\d+[.)]\s*/, "")
|
||
.replace(/\s+/gu, " ")
|
||
.trim()
|
||
.toLowerCase();
|
||
}
|
||
|
||
/** Der Schluessel, unter dem ein Haken gespeichert wird.
|
||
*
|
||
* AUS DEM TEXT, NICHT AUS DER ZEILENNUMMER -- die Begruendung steht
|
||
* bei der Tabelle `event_punkte` in workspace.js. Hier nur das Wie:
|
||
* Kern bilden, davon die ersten 16 Zeichen des SHA-256. 16 Zeichen
|
||
* sind 64 Bit; bei hoechstens ein paar Dutzend Punkten je Event ist
|
||
* eine Verwechslung ausgeschlossen, und der Schluessel bleibt kurz
|
||
* genug, um in einer Adresse zu stehen. */
|
||
export function punktSchluessel(zeile) {
|
||
return createHash("sha256").update(punktKern(zeile), "utf8")
|
||
.digest("hex").slice(0, 16);
|
||
}
|
||
|
||
/** Zerlegt ein Textfeld in Punkte.
|
||
*
|
||
* LEERE ZEILEN FALLEN WEG, aber nicht die Absicht dahinter: Wer eine
|
||
* Leerzeile zwischen zwei Bloecke setzt, trennt -- und das ergibt in
|
||
* einer Liste ohnehin keinen eigenen Punkt.
|
||
*
|
||
* GLEICHE PUNKTE NUR EINMAL: Stuende „22 Live-Stunden" zweimal da,
|
||
* haetten beide denselben Schluessel. Ein Haken wuerde dann an zwei
|
||
* Stellen gleichzeitig erscheinen und die Zaehlung „2 von 3" waere
|
||
* falsch. Der zweite faellt deshalb weg -- zusammen mit der Zeile,
|
||
* denn eine doppelte Bedingung ist ohnehin ein Tippfehler. */
|
||
export function punkteAusText(text) {
|
||
const raus = [];
|
||
const gesehen = new Set();
|
||
for (const zeile of String(text || "").split(/\r?\n/)) {
|
||
const kern = punktKern(zeile);
|
||
if (!kern) continue;
|
||
const schluessel = punktSchluessel(zeile);
|
||
if (gesehen.has(schluessel)) continue;
|
||
gesehen.add(schluessel);
|
||
raus.push({
|
||
schluessel,
|
||
/* Angezeigt wird der Text OHNE das getippte Aufzaehlungszeichen:
|
||
Die Liste setzt ihr eigenes, und zwei davon nebeneinander
|
||
sehen nach einem Fehler aus. */
|
||
text: String(zeile).replace(PUNKT_ZEICHEN, "").trim(),
|
||
});
|
||
}
|
||
return raus;
|
||
}
|
||
|
||
const jetzt = () => new Date().toISOString();
|
||
|
||
function angemeldet(req, res, next) {
|
||
const person = sitzungLesen(req);
|
||
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
|
||
req.person = person;
|
||
next();
|
||
}
|
||
|
||
function gleicheHerkunft(req, res, next) {
|
||
const herkunft = req.get("origin");
|
||
if (!herkunft) return next();
|
||
let erlaubt;
|
||
try { erlaubt = new URL(herkunft).host === req.get("host"); } catch { erlaubt = false; }
|
||
if (!erlaubt) return res.status(403).json({ fehler: "fremde_herkunft" });
|
||
next();
|
||
}
|
||
|
||
bereicheRouter.use("/workspace/api/bereich", angemeldet);
|
||
|
||
/* =====================================================================
|
||
DAS IDEEN-BOARD GIBT ES NUR FUER DIE, DIE ES SEHEN DUERFEN
|
||
(10.09.2026)
|
||
|
||
404 und nicht 403: "Kein Zugriff" waere die Auskunft, dass es den
|
||
Bereich gibt -- und wer nach "warum darf ich das nicht" fragt, fragt
|
||
als naechstes "wer denn dann". Ein Bereich, den es fuer mich nicht
|
||
gibt, wirft keine Fragen auf. Wortgleich mit der Antwort auf einen
|
||
erfundenen Bereichsnamen (siehe `if (!BEREICHE[bereich])` weiter
|
||
unten).
|
||
|
||
ALS EIGENE HUERDE UND NICHT ALS ABFRAGE IN DEN VIER HANDLERN: Es gibt
|
||
Lesen, Anlegen, Aendern und Loeschen. Vier Abfragen sind vier
|
||
Gelegenheiten, eine zu vergessen -- und die vergessene faellt nicht
|
||
auf, weil an ihrer Stelle nichts steht. Der Pfad-Praefix greift auch
|
||
fuer die Wege mit einer Nummer dahinter.
|
||
|
||
ZWEI SCHLOESSER, ABSICHTLICH: Hier haengt der ZUGANG zur Seite,
|
||
sichtbar() haengt die Bedingung an die ZEILEN. Faellt eines weg,
|
||
haelt das andere. */
|
||
for (const geschuetzt of ["ideen", "angebote", "rueckmeldung"]) {
|
||
bereicheRouter.use(`/workspace/api/bereich/${geschuetzt}`, (req, res, next) => {
|
||
if (siehtModis(req.person)) return next();
|
||
return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
});
|
||
}
|
||
|
||
/* ---------------------------------------------------------------------
|
||
ZWEI BEREICHE GEHEN NUR DOGFATHER UND DIE RECHTE HAND AN (11.09.2026)
|
||
|
||
Filipe: "wo WIR aufgaben oder bewertungen ueber modis eingeben
|
||
koennen." Wir heisst hier: die beiden.
|
||
|
||
`siehtModis` waere zu weit -- darin sind die Modis selbst enthalten,
|
||
und in "Entwicklung" steht, was ueber sie aufgeschrieben wird.
|
||
`fuehrtTeamDogi` ist die engere Menge und dieselbe, die schon den
|
||
Eingang und die Kanaele traegt.
|
||
|
||
404 UND NICHT 403: Ein Modi soll nicht erfahren, dass es diese
|
||
Bereiche gibt. Ein "das darfst du nicht" waere die Auskunft, die
|
||
genau die Frage aufwirft, die niemand stellen soll.
|
||
|
||
ZWEI SCHLOESSER WIE OBEN: Hier haengt der ZUGANG, die Sichtbarkeit
|
||
der Zeilen haengt an sichtbarEintrag(). Faellt eines weg, haelt das
|
||
andere. */
|
||
for (const nurLeitung of ["entwicklung", "talente"]) {
|
||
bereicheRouter.use(`/workspace/api/bereich/${nurLeitung}`, (req, res, next) => {
|
||
if (fuehrtTeamDogi(req.person)) return next();
|
||
return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
});
|
||
}
|
||
|
||
/* ---------------------------------------------------------------------
|
||
UND UMGEKEHRT: Ein Modi kommt nur in SEINE Bereiche.
|
||
|
||
Seit dem 10.09.2026 darf er bereich.html oeffnen -- ohne diese Huerde
|
||
auch `?b=agentur`, die Ablage der Agentur, die ihn nichts angeht. Die
|
||
Kacheln bieten sie ihm nicht an; eine Regel, die nur im Formular
|
||
gilt, ist aber keine.
|
||
|
||
Welche erlaubt sind, steht nicht hier, sondern wird aus seinen
|
||
Kacheln abgeleitet (MODI_BEREICHE_ERLAUBT). Zwei Listen waeren die
|
||
Stelle, an der es auseinanderlaeuft.
|
||
|
||
404 wie ueberall: Ein Bereich, den es fuer mich nicht gibt, wirft
|
||
keine Fragen auf. */
|
||
bereicheRouter.use("/workspace/api/bereich/:bereich", (req, res, next) => {
|
||
/* Auch die Stellvertretung. Sie hat dieselben Kacheln wie ein Modi
|
||
und damit dieselben erlaubten Bereiche -- ohne diese Zeile kaeme
|
||
sie ueber die Adresszeile in die Agentur-Ablage, die sie nichts
|
||
angeht. Genau die Luecke, gegen die diese Schranke gebaut wurde,
|
||
nur eine Rolle spaeter. */
|
||
const b = String(req.params.bereich);
|
||
const nein = () => res.status(404).json({ fehler: "nicht_gefunden" });
|
||
|
||
/* EINE AUSSENROLLE KOMMT NUR IN DEN TREFF (11.09.2026).
|
||
|
||
Und zwar als ERSTER Zweig, nicht als letzter. Die Zeile darunter
|
||
hiess bis heute "wer nicht zum Team gehoert, darf weiter" -- fuer
|
||
einen Gast waere das ein Freifahrtschein durch alle achtzehn
|
||
Bereiche gewesen, obwohl die Kacheln ihm nur sieben anbieten. Eine
|
||
Regel, die nur im Formular gilt, ist keine Regel. */
|
||
if (AUSSEN_ROLLEN.has(req.person?.rolle)) {
|
||
return TREFF_BRETTER.includes(b) ? next() : nein();
|
||
}
|
||
|
||
/* UND UMGEKEHRT: Wer nicht zum Treff gehoert, kommt nicht hinein.
|
||
Das ist derselbe Riegel wie in sichtbarEintrag(), nur eine Ebene
|
||
frueher -- dort entscheidet er, welche ZEILEN jemand sieht, hier,
|
||
ob die Seite ueberhaupt antwortet. Zwei Schloesser, absichtlich:
|
||
Faellt eines weg, haelt das andere. */
|
||
if (TREFF_BRETTER.includes(b) && !TREFF_ROLLEN.has(req.person?.rolle)) return nein();
|
||
|
||
if (!TEAM_DOGI_ROLLEN.has(req.person?.rolle)) return next();
|
||
/* Bretter mit eigener Kachel: wer die Kachel hat, darf hinein. */
|
||
if (MODI_BEREICHE_ERLAUBT.has(b)) return next();
|
||
/* Bretter HINTER einer Seite (entwicklung, talente): nur fuer den,
|
||
der die Seite selbst oeffnen darf. Bis zum 19.09.2026 stand hier
|
||
eine feste Liste ohne Rollenfrage -- ein Modi kam damit an die
|
||
Notizen zu Kandidaten, obwohl talente.html fuer ihn gesperrt ist. */
|
||
if (brettHinterSeiteOffen(req.person, b)) return next();
|
||
return nein();
|
||
});
|
||
|
||
/* ---------- Die Bereiche sind fuer Creator zum LESEN da ----------------
|
||
|
||
Wunsch Filipe, 31.08.2026: "die creator sollen da nur die sehen die wir
|
||
ihnen eintragen, also dogfather manager und scout."
|
||
|
||
Vorher durfte ein Creator hier selbst Eintraege anlegen, aendern und
|
||
die eigenen wieder loeschen. Das klingt harmlos, hebelt aber den Zweck
|
||
dieser Bereiche aus: Sie sind die Betreuungsakte -- LIVE-Auswertungen,
|
||
Technikbefunde, Konfliktverlaeufe, Schutzvorfaelle. Was dort steht,
|
||
muss der Stand der Betreuung sein und nicht das, was jemand ueber sich
|
||
selbst notiert hat. Ein Creator, der eine Auswertung umschreiben oder
|
||
eine unbequeme Notiz loeschen kann, macht die Akte wertlos.
|
||
|
||
Der Creator SIEHT weiterhin alles, was zu ihm gehoert -- vollstaendig,
|
||
nichts wird ihm vorenthalten. Er kann es nur nicht veraendern.
|
||
|
||
Bewusst 403 mit klarem Text und nicht 404: Der Bereich existiert fuer
|
||
ihn ja, er steht sogar davor. Ein 404 wuerde hier luegen. (Bei etwas,
|
||
das er gar nicht sehen darf, bleibt es weiterhin 404.)
|
||
|
||
Steht als EINE Schranke vor allen schreibenden Wegen. Die Alternative
|
||
-- in jeder der drei Stellen einzeln pruefen -- ist genau die Bauweise,
|
||
durch die am 31.08.2026 schon einmal ein Loch entstanden ist: Zwei von
|
||
drei Stellen waren abgesichert, die dritte vergessen. */
|
||
/* Ein Creator schreibt hier nicht -- MIT EINER AUSNAHME, die am
|
||
01.09.2026 dazukam: Was er SELBST angelegt hat, darf er auch
|
||
aendern und loeschen.
|
||
|
||
Die urspruengliche Regel ("hier tragen deine Betreuer ein") bleibt
|
||
damit unangetastet: Er kann weiterhin nichts an dem aendern, was
|
||
ueber ihn geschrieben wurde -- keine Bewertung, keine Notiz, keinen
|
||
Eintrag seines Managers.
|
||
|
||
Noetig wurde die Ausnahme durch die fertigen Vorlagen: Wenn ein
|
||
Creator sich eine Content-Idee uebernimmt, ist das SEINE Idee. Sie
|
||
danach nicht umbenennen zu duerfen waere absurd -- er haette einen
|
||
Eintrag erzeugt, den nur ein anderer bearbeiten kann.
|
||
|
||
Entscheidend ist erstellt_von, nicht creator_id: Ein Eintrag, den
|
||
sein Betreuer ueber ihn angelegt hat, traegt zwar seine creator_id,
|
||
gehoert ihm aber nicht. */
|
||
function nichtSchreibendFuerCreator(req, res, next) {
|
||
if (req.person?.rolle !== "creator") return next();
|
||
|
||
/* Neu anlegen bleibt gesperrt -- der Weg dafuer ist "Vorlage
|
||
uebernehmen", und der prueft eigene Regeln. */
|
||
/* NUR im Bereich content. Das ist der Bereich, in dem der Creator
|
||
selbst arbeitet -- seine Ideen, sein Material. LIVE, Technik,
|
||
Community und Schutz sind die BETREUUNGSAKTE: Dort schreiben die
|
||
Betreuer ueber ihn, und daran aendert er nichts, auch nicht an
|
||
etwas, das frueher einmal von ihm stammte. Das war am 31.08.2026
|
||
ausdruecklich so entschieden und bleibt so.
|
||
|
||
Ein erster Anlauf hatte die Ausnahme fuer ALLE Bereiche erlaubt --
|
||
die Pruefung pruef-bereiche-lesend hat das sofort gemeldet. */
|
||
const bereich = String(req.path).split("/")[4] || "";
|
||
if (bereich !== "content") {
|
||
return res.status(403).json({
|
||
fehler: "Hier tragen deine Betreuer ein. Du siehst alles, was zu dir gehört.",
|
||
});
|
||
}
|
||
|
||
const id = Number(String(req.path).split("/").pop());
|
||
if (Number.isInteger(id) && id > 0) {
|
||
try {
|
||
const e = db().prepare("SELECT erstellt_von FROM eintraege WHERE id = ?").get(id);
|
||
if (e && e.erstellt_von === req.person.id) return next();
|
||
} catch { /* faellt unten auf die Sperre zurueck */ }
|
||
}
|
||
|
||
return res.status(403).json({
|
||
fehler: "Das hat deine Betreuung eingetragen – ändern können sie es. "
|
||
+ "Was du selbst angelegt hast, kannst du bearbeiten.",
|
||
});
|
||
}
|
||
|
||
/* Express 4 -- dort ist der Platzhalter `*`, nicht `*name` wie in
|
||
Fassung 5. Mit der Fassung-5-Schreibweise haette der Pfad wortwoertlich
|
||
gepasst werden muessen und die Schranke waere NIE gegriffen: kein
|
||
Fehler, keine Warnung, nur eine Sperre, die stumm nichts tut. */
|
||
for (const weg of ["post", "patch", "delete", "put"]) {
|
||
bereicheRouter[weg]("/workspace/api/bereich/*", nichtSchreibendFuerCreator);
|
||
}
|
||
|
||
/* =====================================================================
|
||
DIE UMHUELLUNG: was einem Modi gehoert, faellt hier heraus.
|
||
|
||
Sie sitzt UM die eigentliche Regel und nicht darin -- damit sie auch
|
||
fuer Regeln gilt, die es heute noch nicht gibt. Beim ersten Anlauf
|
||
war nur der eine bekannte Fall geflickt (Spicy Media); das haette den
|
||
naechsten Rollenzweig wieder offen gelassen, und niemand haette es
|
||
gemerkt, weil an der geaenderten Stelle nichts davon steht.
|
||
|
||
DogFather und die Modis gehen unveraendert durch. Fuer alle anderen
|
||
kommt die Bedingung dazu -- auch fuer die, deren Regel ohnehin nichts
|
||
Fremdes trifft. Das kostet eine Unterabfrage und spart die Frage,
|
||
ob es diesmal wirklich niemand treffen kann.
|
||
===================================================================== */
|
||
export function sichtbar(person) {
|
||
const regel = sichtbarRoh(person);
|
||
if (!regel) return regel;
|
||
/* AUF DER ADRESSE VON TEAM DOGI NUR TEAM DOGI (10.09.2026).
|
||
|
||
Filipe: "die daten von dieser seite sollen nichts mit den daten am
|
||
hut haben von der workspace seite ... komplett von der anderen
|
||
getrennt sein."
|
||
|
||
Die Bedingung steht VOR der Regel fuer das andere Haus und nicht
|
||
danach: Beide schliessen sich aus, und wer sie hintereinander
|
||
haengt, bekommt auf der Team-Adresse eine Bedingung, die sich
|
||
selbst widerspricht -- und damit eine leere Seite, die aussieht
|
||
wie ein Fehler.
|
||
|
||
`person.haus` setzt sitzungLesen() aus der Adresse. Es aendert
|
||
keine Rechte, nur den Ausschnitt. */
|
||
if (person.haus === "crew") {
|
||
return {
|
||
wo: `(${regel.wo}) AND ${ohneAgentur("e", ["creator_id", "erstellt_von"])}`,
|
||
werte: regel.werte,
|
||
};
|
||
}
|
||
/* ==== DREI FAELLE UND NICHT ZWEI (24.09.2026, nachgebessert) =====
|
||
|
||
Hier stand bis heute `if (siehtModis(person)) return regel;` --
|
||
"wer Modi-Daten sehen darf, bekommt sie ungefiltert". Auf der
|
||
AGENTURADRESSE hiess das in der Praxis: DogFather sieht dort auch
|
||
Team Dogi. Genau das hat Filipe am 24.09.2026 abgeschafft.
|
||
|
||
DIE ERSTE FASSUNG DIESER REPARATUR WAR ZU GROB: Ich habe die Zeile
|
||
einfach gestrichen. Damit fiel auch der Fall "keine der beiden
|
||
Adressen" durch -- also jede Pruefadresse. Gemessen: 19
|
||
Fehlschlaege in drei Pruefungen, weil ein Modi seine eigenen
|
||
Aufgaben nicht mehr sah. Nicht weil etwas kaputt war, sondern weil
|
||
`ohneTeamDogi` dort ploetzlich auf ihn selbst zeigte.
|
||
|
||
ES SIND DREI LAGEN, und jede bekommt ihre Zeile:
|
||
crew -> ohne die Agentur
|
||
agentur -> ohne Team Dogi, AUCH fuer DogFather (das ist neu)
|
||
kein Haus (localhost, Pruefungen) -> die alte Rollenregel,
|
||
unveraendert. Sonst waeren alle Modi-Pruefungen des
|
||
Hauses still gruen und blind -- die Falle, vor der
|
||
crew-adresse.js dreimal warnt.
|
||
|
||
`siehtModis` BLEIBT UNVERAENDERT und wird anderswo weiter
|
||
gebraucht (Rechtefragen, z.B. in der Checkliste). "Darf ich das
|
||
sehen" und "welchen Ausschnitt sehe ich gerade" sind zwei Fragen;
|
||
wer sie in eine Funktion legt, kann spaeter nicht mehr sagen,
|
||
welche zugeschlagen hat. */
|
||
if (person.haus === "agentur") {
|
||
return {
|
||
wo: `(${regel.wo}) AND ${ohneTeamDogi("e", ["creator_id", "erstellt_von"])}`,
|
||
werte: regel.werte,
|
||
};
|
||
}
|
||
if (siehtModis(person)) return regel;
|
||
return {
|
||
wo: `(${regel.wo}) AND ${ohneTeamDogi("e", ["creator_id", "erstellt_von"])}`,
|
||
werte: regel.werte,
|
||
};
|
||
}
|
||
|
||
/* Scouts haben mit der Creator-Betreuung nichts zu tun -- sie sehen hier
|
||
nichts. Creator sehen ihren eigenen Bereich, das Management alles. */
|
||
function sichtbarRoh(person) {
|
||
/* 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. */
|
||
if (istSpicy(person)) return { wo: ohneDogFather("e"), werte: [] };
|
||
if (siehtAlles(person)) return { wo: "1=1", werte: [] };
|
||
|
||
/* "ODER ich habe ihn selbst angelegt" kam am 02.09.2026 dazu -- aus
|
||
demselben Grund wie bei den Aufgaben: Ein Eintrag ohne creator_id
|
||
(Feld auf "—" gelassen oder seit heute ein freier Name) traf keine
|
||
der Bedingungen und war fuer den, der ihn gerade geschrieben hatte,
|
||
sofort unsichtbar. Nur DogFather sah ihn noch.
|
||
|
||
Es sieht weiterhin niemand etwas Fremdes -- nur das Eigene. */
|
||
if (person.rolle === "creator") {
|
||
return { wo: "(e.creator_id = ? OR e.erstellt_von = ?)", werte: [person.id, person.id] };
|
||
}
|
||
/* Ein Scout sieht die Bereiche der Creator, die er betreut -- dazu,
|
||
was er selbst eingetragen hat. */
|
||
/* DAS MODI-TEAM TEILT SICH SEINE EINTRAEGE -- wie die Aufgaben
|
||
(Entscheidung Filipe, 09.09.2026: "sie sind untereinander ein
|
||
Team"). Ohne diesen Zweig saehe jeder Modi nur, was er selbst
|
||
geschrieben hat, und eine gemeinsame Ideensammlung waere keine. */
|
||
/* NACHTRAG 10.09.2026: Die Stellvertretung gehoert in denselben
|
||
Zweig. Ohne sie faellt sie unten durch und landet bei der
|
||
Betreuungsregel -- die fuer sie nichts findet. Ergebnis waere ein
|
||
LEERES Ideen-Board und ein leeres Angebote-Brett, und das meldet
|
||
niemand: Ein leeres Brett sieht nicht nach Fehler aus. Genau so ist
|
||
es am 01.09. dem Manager und am 09.09. dem Modi ergangen. */
|
||
if (TEAM_DOGI_ROLLEN.has(person.rolle)) {
|
||
/* UND DOGFATHER GEHOERT DAZU (17.09.2026).
|
||
|
||
Filipe: "die community und alle anderen sollen die neuen sachen
|
||
in allen kategorien auch sehen bitte. in allen die sie sehen
|
||
soll auch jede aenderung mit gehen, immer nur in denen die ihnen
|
||
erlaubt ist zu sehen."
|
||
|
||
GEMESSEN, und der Befund war groesser als der Anlass:
|
||
TEAM_DOGI_ROLLEN ist {hand, modi} -- ohne "admin". Die Bedingung
|
||
hier lautete damit woertlich "Eintraege von hand oder modi", und
|
||
DogFather fiel heraus. Folge: ALLES, was er in Live, Technik,
|
||
Community, Ideen, Angebote oder Rueckmeldung schreibt, war fuer
|
||
sein eigenes Team unsichtbar.
|
||
|
||
Bereich DogFather rechte Hand Modi
|
||
live 8 0 0
|
||
technik 8 0 0
|
||
community 8 0 0
|
||
ideen 5 0 0
|
||
|
||
Das ist nicht erst seit dem Startkatalog so -- der hat es nur
|
||
sichtbar gemacht, weil vorher ueberall Null stand und Null gleich
|
||
Null aussieht, egal aus welchem Grund.
|
||
|
||
WARUM NICHT "admin" IN TEAM_DOGI_ROLLEN AUFNEHMEN: Diese Menge
|
||
entscheidet an anderer Stelle, WELCHES HAUS jemand bewohnt
|
||
(crew statt agentur) und was Spicy Media NICHT sehen darf. Ein
|
||
Eintrag dort haette DogFather stillschweigend ins andere Haus
|
||
verschoben. Die Liste wird deshalb genau hier erweitert, wo es
|
||
um Sichtbarkeit von Eintraegen geht -- und nirgends sonst.
|
||
|
||
UND ES OEFFNET NICHTS: Welche Bereiche ein Modi ueberhaupt
|
||
aufrufen darf, entscheidet der Riegel weiter oben
|
||
(MODI_BEREICHE_ERLAUBT plus brettHinterSeiteOffen)
|
||
(content, schutz, agentur, entwicklung, talente bleiben 404).
|
||
Die Agentur-Trennung haengt an ohneAgentur() und AGENTUR_ROLLEN
|
||
-- dort steht "admin" nicht drin, an ihr aendert sich also
|
||
ebenfalls nichts. */
|
||
const liste = [...TEAM_DOGI_ROLLEN, "admin"].map((r) => `'${r}'`).join(", ");
|
||
return { wo: `(e.erstellt_von IN (SELECT id FROM personen WHERE rolle IN (${liste}))`
|
||
+ ` OR e.creator_id IN (SELECT id FROM personen WHERE rolle IN (${liste})))`, werte: [] };
|
||
}
|
||
const b = betreutWo(person, "e.creator_id");
|
||
return b
|
||
? { wo: `(e.erstellt_von = ? OR ${b.wo})`, werte: [person.id, ...b.werte] }
|
||
: { wo: "e.erstellt_von = ?", werte: [person.id] };
|
||
}
|
||
|
||
/* Dieselbe Regel, aber mit der Ausnahme fuer Bereiche, die allen
|
||
gehoeren.
|
||
|
||
BEWUSST EINE EIGENE FUNKTION, NICHT EINE AENDERUNG AN sichtbar():
|
||
sichtbar() beantwortet auch Fragen zu Aufgaben, Dateien, Terminen und
|
||
Calls (siehe die Aufrufe in workspace-aufgaben.js,
|
||
workspace-dateien.js, workspace-kalender.js, workspace-calls.js). Wer
|
||
dort "1=1" einschleust, weil er an Agentur-Eintraege dachte, oeffnet
|
||
nebenbei fremde Dateien und fremde Termine -- ein Loch, das man dem
|
||
Code nicht ansieht, weil an der geaenderten Stelle nichts davon
|
||
steht.
|
||
|
||
Nur Eintraege gehen hier durch, und nur die aus einem Bereich mit
|
||
`fuerAlle`. Der Rest faellt woertlich auf die alte Regel zurueck.
|
||
|
||
`praefix` ist da, weil die Suche dieselbe Tabelle unter demselben
|
||
Kuerzel "e" fuehrt -- geht das eines Tages auseinander, faellt es
|
||
beim Aufruf auf, nicht erst im Betrieb. */
|
||
/* VERTRAULICHE RUECKMELDUNGEN SIEHT NUR DOGFATHER -- UND DER SCHREIBER
|
||
(10.09.2026).
|
||
|
||
Wer beim Schreiben "nur an DogFather" waehlt, muss sich darauf
|
||
verlassen koennen. Deshalb steht die Bedingung HIER, in derselben
|
||
Funktion, durch die auch das Lesen einer einzelnen Zeile, das Aendern
|
||
und das Loeschen gehen -- nicht in der Listenabfrage. Eine Regel, die
|
||
nur die Liste filtert, laesst die Zeile ueber ihre Nummer trotzdem
|
||
heraus.
|
||
|
||
"NUR DOGFATHER" HEISST AUCH NICHT DIE RECHTE HAND. Sie sieht sonst
|
||
ueberall dasselbe wie er. Hier nicht, und zwar deshalb, weil das
|
||
Etikett sonst nicht stimmte: Eine Zusage mit einer Ausnahme im
|
||
Kleingedruckten ist keine. Sie kann selbst genauso schreiben -- auch
|
||
ueber ihn, und dann sieht er es nicht anders als jeder andere.
|
||
|
||
`COALESCE(..., 0)`: Alle Zeilen, die es vor heute gab, haben in
|
||
dieser Spalte NULL. Ohne den Ersatzwert waere `nur_leitung = 0`
|
||
fuer sie UNBEKANNT statt wahr -- und der gesamte alte Bestand waere
|
||
von einer Minute auf die andere unsichtbar gewesen. Ein Loch, das
|
||
niemand meldet: Ein leeres Brett sieht nicht nach Fehler aus. */
|
||
function ohneVertrauliche(regel, person, praefix) {
|
||
if (!regel || istDogFather(person)) return regel;
|
||
return {
|
||
wo: `(${regel.wo}) AND (COALESCE(${praefix}.nur_leitung, 0) = 0`
|
||
+ ` OR ${praefix}.erstellt_von = ?)`,
|
||
werte: [...regel.werte, person.id],
|
||
};
|
||
}
|
||
|
||
/* =====================================================================
|
||
DER TREFF HAT ZWEI RIEGEL, UND SIE ZEIGEN IN ENTGEGENGESETZTE
|
||
RICHTUNGEN (11.09.2026)
|
||
|
||
Filipe: "wo die community auch dan nur die sieht und die modis
|
||
rechte hand und ich also dogfather sehen die aber auch. die
|
||
community aber nur die dann."
|
||
|
||
Daraus folgen zwei Saetze, und beide muessen gelten:
|
||
|
||
1. EINE AUSSENROLLE SIEHT NUR DEN TREFF. Nicht "den Treff und was
|
||
sonst noch durchfaellt" -- nur ihn. Deshalb steht dieser Zweig
|
||
GANZ VORN und baut seine Regel selbst, statt auf der
|
||
allgemeinen aufzusetzen. Wer auf einer bestehenden Regel
|
||
aufsetzt, erbt jeden Zweig, den sie hat, auch die, die es
|
||
morgen erst gibt.
|
||
|
||
2. DIE AGENTUR SIEHT DEN TREFF NICHT. Creator, Scouts, Manager und
|
||
Spicy Media haben mit den Zuschauern dieses Streams nichts zu
|
||
tun. Ohne diesen zweiten Riegel haette `fuerAlle` sie
|
||
hereingelassen -- genau das Loch, das am 10.09. schon einmal
|
||
entstanden ist, nur andersherum.
|
||
|
||
BEIDE STEHEN HIER UND NICHT IN sichtbar(). Diese Funktion beantwortet
|
||
auch Fragen zu Aufgaben, Dateien und Terminen; ein Bereichsname hat
|
||
dort nichts zu suchen. Der Kommentar eine Bildschirmseite weiter oben
|
||
warnt woertlich davor. */
|
||
|
||
/* =====================================================================
|
||
RIEGEL 0: DAS HAUS DER ADRESSE (03.10.2026)
|
||
|
||
Filipe, 24.09.2026: "bevor du das aber machst will ich dass du
|
||
zuerst die komplette site vn der workspace seite trennst. da soll
|
||
nichts verknuepft sein. ... vermerk dass ab jetzt jeder umbau immer
|
||
getrennt abgefuehrt wird auf den zwei seiten."
|
||
|
||
WAS GEMESSEN WURDE (03.10.2026, server/_mess-haeuser.mjs, jedes
|
||
Brett x beide Adressen x vier Rollen):
|
||
|
||
AGENTUR-Adresse DogFather 18 Bretter
|
||
Manager 6 Bretter
|
||
Creator 6 Bretter
|
||
Modi kommt nicht rein (401)
|
||
CREW-Adresse DogFather 18 Bretter
|
||
Modi 13 Bretter
|
||
Manager kommt nicht rein (401)
|
||
Creator kommt nicht rein (401)
|
||
|
||
Die Anmeldung ist also dicht -- eine Agenturrolle kommt nicht auf
|
||
die Crew-Adresse und umgekehrt. Durch greift genau EINE Rolle:
|
||
DogFather. Er ist die einzige in BEIDE_HAEUSER_ROLLEN, und die
|
||
Riegel darunter fragen nach der ROLLE, nicht nach der Adresse.
|
||
Auf der Crew-Adresse stand damit das Brett der Agentur samt Inhalt.
|
||
|
||
WARUM ES BISHER NICHT GRIFF, obwohl es die Spalte `eintraege.haus`
|
||
seit dem 24.09. gibt: Sie wird beim ANLEGEN gefuellt und beim LESEN
|
||
nie gefragt. Gefiltert wurde ausschliesslich ueber die ROLLEN der
|
||
beteiligten Personen (ohneTeamDogi / ohneAgentur) -- und ein Eintrag,
|
||
an dem nur DogFather haengt, faellt durch beide Netze, weil er in
|
||
beiden Haeusern wohnt. Bei 20 von 26 Eintraegen ist er der Ersteller.
|
||
|
||
DER RIEGEL STEHT GANZ VORNE, vor der Aussenrolle: Er gilt damit fuer
|
||
JEDEN Rueckgabeweg dieser Funktion, auch fuer den, den jemand als
|
||
naechstes einbaut. Dieselbe Ueberlegung wie bei "WARUM DER KLICK
|
||
GLEICH HIER DRANHAENGT" in bereich.js -- eine Funktion mit drei
|
||
Ausgaengen braucht ihre Regel am Eingang, nicht an jedem Ausgang.
|
||
|
||
OHNE ADRESSE KEINE EINSCHRAENKUNG. `person.haus` setzt sitzungLesen()
|
||
aus dem Host-Kopf; bei einer Pruefung auf 127.0.0.1 steht dort
|
||
nichts. Dann greift dieser Riegel nicht -- sonst waeren alle
|
||
Pruefungen des Hauses still gruen und blind, genau die Falle, vor der
|
||
crew-adresse.js dreimal warnt.
|
||
|
||
UND NULL IST KEIN LOCH. Fuenf von acht Stellen, die Eintraege
|
||
anlegen, setzen `haus` gar nicht (gemessen: workspace-video.js,
|
||
workspace-content.js, workspace-bewerbung.js, workspace-treff.js,
|
||
workspace-vorlagen.js). Ein Filter, der nur `haus = ?` prueft,
|
||
wuerde deren Eintraege fuer BEIDE Haeuser verbergen -- Daten
|
||
verschwinden still, der schlimmere Ausgang. Deshalb entscheidet bei
|
||
NULL der BEREICH, und zwar nach genau derselben Regel, nach der der
|
||
naechtliche Nachtrag ihn setzt (eintraegeNachHaus in workspace.js).
|
||
Zwei Fassungen derselben Regel waeren die, die auslaeuft.
|
||
===================================================================== */
|
||
|
||
/** Die Bedingung "nur Eintraege dieses Hauses" -- oder null, wenn die
|
||
* Frage sich nicht stellt (keine Adresse, also keine Haustrennung).
|
||
*
|
||
* ==== WARUM NULL SICHTBAR BLEIBT (03.10.2026) =======================
|
||
*
|
||
* Die erste Fassung hat bei `haus IS NULL` den BEREICH entscheiden
|
||
* lassen -- nach derselben Regel, nach der eintraegeNachHaus() das
|
||
* Haus nachtraegt (MODI_BEREICHE_ERLAUBT). Das war falsch, und
|
||
* pruef-haus-trennung.mjs hat es sofort gefunden:
|
||
*
|
||
* FEHL auf workspace. steht der Eintrag da
|
||
*
|
||
* Es ging um "Lunas Live vom Montag" auf dem Brett `live`. Die
|
||
* Ableitung stuft `live` als Crew-Brett ein (Team Dogi hat eine
|
||
* Kachel darauf) -- aber dasselbe Brett traegt auch die
|
||
* Creator-Akten der Agentur. `live`, `technik` und `community`
|
||
* gehoeren BEIDEN Haeusern, und eine Regel, die jedem Brett genau
|
||
* ein Haus zuweist, kann das nicht abbilden.
|
||
*
|
||
* Sie ist als VERTEILREGEL trotzdem richtig: Wenn ein neuer Eintrag
|
||
* ein Haus braucht, ist "wer hat die Kachel" die beste Auskunft, die
|
||
* es gibt. Als AUSSCHLUSSREGEL beim Lesen ist sie es nicht -- dort
|
||
* nimmt sie etwas weg, statt etwas zuzuordnen, und ein Irrtum kostet
|
||
* Sichtbarkeit statt nur Ordnung.
|
||
*
|
||
* Deshalb: Wer ein Haus hat, wird danach gefiltert. Wer keins hat,
|
||
* bleibt sichtbar. Das ist die sichere Richtung -- ein Eintrag zu
|
||
* viel faellt auf, ein fehlender nicht.
|
||
*
|
||
* DAMIT DARAUS KEIN DAUERLOCH WIRD, zwei Dinge: Die fuenf Stellen,
|
||
* die `haus` bisher nicht gesetzt haben, tun es jetzt (gemessen am
|
||
* 03.10.2026: workspace-video.js, workspace-content.js,
|
||
* workspace-bewerbung.js, workspace-treff.js, workspace-vorlagen.js),
|
||
* und pruef-haus-luecke.mjs wird rot, sobald eine sechste dazukommt,
|
||
* die es vergisst. Was trotzdem ohne Haus entsteht, traegt
|
||
* eintraegeNachHaus() beim naechsten Start nach. */
|
||
function nurDiesesHaus(person, praefix = "e") {
|
||
const haus = person?.haus;
|
||
if (haus !== "crew" && haus !== "agentur") return null;
|
||
return {
|
||
wo: `(${praefix}.haus = ? OR ${praefix}.haus IS NULL)`,
|
||
werte: [haus],
|
||
};
|
||
}
|
||
|
||
export function sichtbarEintrag(person, praefix = "e") {
|
||
/* RIEGEL 0 -- das Haus der Adresse. Begruendung direkt darueber.
|
||
Er wird den uebrigen Riegeln UMGEHANGEN statt in jeden Ausgang
|
||
geschrieben: Diese Funktion hat drei Rueckgabepunkte, und der
|
||
naechste, den jemand einbaut, waere sonst wieder offen. */
|
||
const haus = nurDiesesHaus(person, praefix);
|
||
const mitHaus = (r) => {
|
||
if (!r || !haus) return r;
|
||
return { wo: `(${r.wo}) AND ${haus.wo}`, werte: [...r.werte, ...haus.werte] };
|
||
};
|
||
|
||
/* RIEGEL 1 -- die Aussenrolle. Eine eigene, vollstaendige Regel:
|
||
genau die Bretter des Treffs, sonst nichts. */
|
||
if (AUSSEN_ROLLEN.has(person?.rolle)) {
|
||
const platz = TREFF_BRETTER.map(() => "?").join(", ");
|
||
const grund = { wo: `${praefix}.bereich IN (${platz})`, werte: [...TREFF_BRETTER] };
|
||
/* RIEGEL 1b -- WAS NOCH NICHT FREIGEGEBEN IST, GIBT ES NICHT
|
||
(11.09.2026). Termine und Highlights erscheinen der Community
|
||
erst, wenn jemand aus dem Team sie freigegeben hat.
|
||
|
||
DIE BEDINGUNG HAENGT HIER UND NICHT AM LESE-WEG, weil sie sonst
|
||
nur dort gaelte: Suche, Ausfuhr und jeder kuenftige Weg, der
|
||
sichtbarEintrag benutzt, haetten die unfreigegebenen Zeilen
|
||
weiterhin geliefert. Genau so ist am 10.09. ein Modi-Eintrag
|
||
durch die Suche gekommen, obwohl das Brett zu war. */
|
||
const frei = freigabeBedingung(person, praefix);
|
||
if (!frei) return mitHaus(grund);
|
||
return mitHaus({ wo: `(${grund.wo}) AND ${frei.wo}`, werte: [...grund.werte, ...frei.werte] });
|
||
}
|
||
const regel = sichtbarRoh2(person, praefix);
|
||
/* RIEGEL 2 -- wer nicht zum Treff gehoert, sieht seine Bretter nicht.
|
||
Auch DogFather geht hier durch; er steht in TREFF_ROLLEN und faellt
|
||
deshalb nicht heraus. */
|
||
if (!regel || TREFF_ROLLEN.has(person?.rolle)) return mitHaus(regel);
|
||
const platz = TREFF_BRETTER.map(() => "?").join(", ");
|
||
return mitHaus({
|
||
wo: `(${regel.wo}) AND ${praefix}.bereich NOT IN (${platz})`,
|
||
werte: [...regel.werte, ...TREFF_BRETTER],
|
||
});
|
||
}
|
||
|
||
function sichtbarRoh2(person, praefix = "e") {
|
||
const regel = sichtbar(person);
|
||
if (!regel) return regel;
|
||
if (regel.wo === "1=1") return regel; // DogFather sieht ohnehin alles
|
||
if (!BEREICHE_FUER_ALLE.length) return ohneVertrauliche(regel, person, praefix);
|
||
const liste = BEREICHE_FUER_ALLE.map(() => "?").join(", ");
|
||
const mitAllen = {
|
||
wo: `(${praefix}.bereich IN (${liste}) OR ${regel.wo})`,
|
||
werte: [...BEREICHE_FUER_ALLE, ...regel.werte],
|
||
};
|
||
/* UND DANACH NOCH EINMAL ZU (10.09.2026).
|
||
|
||
sichtbar() haengt die Modi-Bedingung bereits an -- aber das ODER
|
||
hier oeffnet sie wieder: Ein Eintrag in einem `fuerAlle`-Bereich
|
||
kaeme durch, egal wem er gehoert. Heute hat kein Modi eine Kachel
|
||
dorthin; ein Aufruf an der Oberflaeche vorbei braucht sie aber
|
||
nicht. Eine Regel, die nur im Formular gilt, ist keine Regel. */
|
||
if (siehtModis(person)) return ohneVertrauliche(mitAllen, person, praefix);
|
||
return ohneVertrauliche({
|
||
wo: `(${mitAllen.wo}) AND ${ohneTeamDogi(praefix)}`,
|
||
werte: mitAllen.werte,
|
||
}, person, praefix);
|
||
}
|
||
|
||
const SPALTEN = `
|
||
e.id, e.bereich, e.art, e.titel, e.text, e.datum, e.uhrzeit, e.aus_eintrag_id,
|
||
e.quelle_url, e.quelle_kanal, e.quelle_fehlt_seit, e.bewertung,
|
||
e.dringlichkeit, e.status, e.creator_id, e.erstellt, e.erstellt_von, e.geaendert,
|
||
e.hook, e.format, e.saeule_id, e.geplant,
|
||
e.event_ende, e.event_aufgaben, e.event_regeln,
|
||
e.creator_extern,
|
||
/* Was DogFather dafuer tun muesste -- nur bei den Angeboten
|
||
gefuellt, siehe BEREICHE.angebote. */
|
||
e.einsatz,
|
||
/* 1 = nur DogFather darf das lesen. Die Spalte MUSS mitkommen, auch
|
||
wenn die Regel unten schon dafuer sorgt, dass niemand Fremdes die
|
||
Zeile bekommt: Der Schreibende soll an seiner eigenen Zeile sehen,
|
||
dass sie vertraulich ist. Ohne diese Angabe saehe sie aus wie jede
|
||
andere -- und beim naechsten Mal schriebe er offen, was er
|
||
vertraulich meinte. */
|
||
e.nur_leitung,
|
||
/* Wann der Eintrag der Person geschickt wurde -- oder NULL. Die
|
||
Oberflaeche baut daraus entweder einen Knopf oder einen Satz; ohne
|
||
diese Angabe koennte sie nur einen Knopf zeigen, der beim zweiten
|
||
Druecken eine Absage bringt. */
|
||
e.gesendet_am,
|
||
${externSql("pc.name", "e.creator_extern")} AS creator_name,
|
||
pe.name AS erstellt_name,
|
||
(SELECT name FROM content_saeulen s WHERE s.id = e.saeule_id) AS saeule_name,
|
||
(SELECT slot FROM content_saeulen s WHERE s.id = e.saeule_id) AS saeule_slot`;
|
||
|
||
/* Passt die gewaehlte Saeule zu dem Creator, um den es geht? Gibt einen
|
||
Fehlertext zurueck oder null, wenn alles stimmt. */
|
||
function saeulePasst(saeuleId, creatorId) {
|
||
if (!saeuleId) return null;
|
||
const s = db().prepare("SELECT creator_id FROM content_saeulen WHERE id = ?").get(saeuleId);
|
||
if (!s) return "Diese Säule gibt es nicht.";
|
||
if (creatorId && s.creator_id !== creatorId) return "Diese Säule gehört zu einem anderen Creator.";
|
||
return null;
|
||
}
|
||
|
||
/* =====================================================================
|
||
WAS EIN TREFF-BRETT ZUSAETZLICH MITBRINGT (11.09.2026)
|
||
|
||
Drei Angaben, die es nur dort gibt: wie viele "Will ich auch" ein
|
||
Wunsch hat, ob meine Stimme dabei ist, ob die Ansage angeheftet und
|
||
ob der Eintrag freigegeben ist.
|
||
|
||
SIE HAENGEN NICHT AN SPALTEN, sondern werden nur fuer die sieben
|
||
Bretter angehaengt. Drei Unterabfragen je Zeile sind hier
|
||
bedeutungslos (die Tabellen haben eine Handvoll Zeilen), auf den
|
||
Betreuungsbrettern waeren sie es nicht -- und vor allem waeren sie
|
||
dort eine Auskunft ueber etwas, das es in diesem Bereich gar nicht
|
||
gibt.
|
||
|
||
DER ROLLENNAME KOMMT MIT (Entscheidung Filipe, 11.09.2026: "voller
|
||
Rollenname zeigen"). Er kommt als ROLLE, der Klartext wird erst in
|
||
der Antwort gebildet -- eine Zuordnung Rolle -> Name im
|
||
ausgelieferten JavaScript waere eine Liste aller Rollen dieses
|
||
Hauses auf jedem Rechner der Community.
|
||
===================================================================== */
|
||
const TREFF_SPALTEN = `,
|
||
(SELECT COUNT(*) FROM treff_stimmen ts WHERE ts.eintrag_id = e.id) AS stimmen,
|
||
(SELECT COUNT(*) FROM treff_stimmen ts
|
||
WHERE ts.eintrag_id = e.id AND ts.person_id = ?) AS meine_stimme,
|
||
(SELECT COUNT(*) FROM treff_angeheftet ta WHERE ta.eintrag_id = e.id) AS angeheftet,
|
||
(SELECT COUNT(*) FROM treff_freigaben tf WHERE tf.eintrag_id = e.id) AS freigegeben,
|
||
pe.rolle AS erstellt_rolle`;
|
||
|
||
const VERBUND = `
|
||
FROM eintraege e
|
||
LEFT JOIN personen pc ON pc.id = e.creator_id
|
||
LEFT JOIN personen pe ON pe.id = e.erstellt_von`;
|
||
|
||
/* ---------- Lesen ------------------------------------------------------- */
|
||
|
||
bereicheRouter.get("/workspace/api/bereich/:bereich", (req, res) => {
|
||
try {
|
||
const bereich = String(req.params.bereich);
|
||
const einstellung = BEREICHE[bereich];
|
||
if (!einstellung) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
|
||
const regel = sichtbarEintrag(req.sicht || req.person);
|
||
if (!regel) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
|
||
const treffBrett = TREFF_BRETTER.includes(bereich);
|
||
|
||
/* DIE REIHENFOLGE GEHOERT ZUM BRETT (11.09.2026).
|
||
|
||
Am Anschlagbrett steht oben, was angeheftet ist -- sonst waere
|
||
das Anheften eine Zierde. Auf der Wunschliste steht oben, was die
|
||
meisten wollen; genau das ist der Sinn von "Will ich auch", und
|
||
eine Liste nach Datum haette die Abstimmung unsichtbar gemacht.
|
||
Ueberall sonst bleibt es, wie es war. */
|
||
const reihenfolge = bereich === "anschlag"
|
||
? "angeheftet DESC, e.datum DESC, e.id DESC"
|
||
: bereich === "wunsch"
|
||
/* ENTSCHIEDENES NACH UNTEN -- ABER NICHT WEG (17.09.2026).
|
||
|
||
Die Sortierung nach Stimmen gibt es seit dem 11.09. Seit es
|
||
vier Zustaende gibt, reicht sie nicht mehr: Ein gemachter
|
||
Wunsch mit vielen Stimmen stuende dauerhaft ganz oben und
|
||
verdraengte die offenen, ueber die noch zu entscheiden ist.
|
||
|
||
Loeschen waere die falsche Antwort. Gerade der ERFUELLTE
|
||
Wunsch ist der Beweis, dass sich Schreiben lohnt -- er
|
||
gehoert gesehen, nur nicht an der ersten Stelle. */
|
||
? `CASE e.status WHEN 'offen' THEN 0 WHEN 'angenommen' THEN 0 ELSE 1 END,
|
||
stimmen DESC, e.datum DESC, e.id DESC`
|
||
: `CASE e.status WHEN 'offen' THEN 0 ELSE 1 END,
|
||
CASE e.dringlichkeit WHEN 'hoch' THEN 0 WHEN 'mittel' THEN 1 ELSE 2 END,
|
||
e.datum DESC, e.id DESC`;
|
||
|
||
const wer = (req.sicht || req.person)?.id ?? 0;
|
||
const eintraege = db().prepare(`
|
||
SELECT ${SPALTEN}${treffBrett ? TREFF_SPALTEN : ""} ${VERBUND}
|
||
WHERE ${regel.wo} AND e.bereich = ?
|
||
ORDER BY ${reihenfolge}`)
|
||
.all(...(treffBrett ? [wer] : []), ...regel.werte, bereich);
|
||
|
||
/* =================================================================
|
||
DAS ANSCHLAGBRETT HAELT 24 STUNDEN (22.09.2026)
|
||
=================================================================
|
||
|
||
Filipe: „die sachen sollen in der seite vom anschlagbrett immer
|
||
automatisch nach 24h von da verschwinden. weil alles hat seine
|
||
kategorie und was nicht ist auch nicht um ewig auf der seite zu
|
||
bleiben fertig."
|
||
|
||
SEINE BEGRUENDUNG IST DIE REGEL: Ein Anschlagbrett sagt, was
|
||
GERADE gilt. Was laenger gilt, hat seinen eigenen Bereich -- die
|
||
Regeln stehen bei den Regeln, Termine bei „Was ansteht". Eine
|
||
Ansage vom letzten Dienstag, die noch haengt, ist keine Ansage
|
||
mehr, sondern Papier an der Wand.
|
||
|
||
AB WANN DIE 24 STUNDEN LAUFEN: ab `erstellt`, also ab dem
|
||
Augenblick, in dem jemand sie aufgehaengt hat. Nicht ab `datum`
|
||
-- das ist ein vom Team gesetztes Feld und kann in der Zukunft
|
||
liegen (es traegt die Termine). Eine Ansage, die morgen gilt,
|
||
soll morgen verschwinden und nicht uebermorgen.
|
||
|
||
SIE VERSCHWINDET VON DER SEITE, NICHT AUS DER WELT.
|
||
Filipe sagt „von da verschwinden" -- von DA. Geloescht wird
|
||
nichts: Wer eine Ansage geschrieben hat, soll sie
|
||
wiederfinden, und eine Ansage rueckwirkend verschwinden zu
|
||
lassen waere etwas anderes als sie abzuhaengen. Die aelteren
|
||
kommen deshalb mit, aber als `abgehaengt` gekennzeichnet; die
|
||
Seite zeigt sie zugeklappt unter einer Zeile.
|
||
|
||
WARUM DER SERVER DAS ENTSCHEIDET UND NICHT DER BROWSER: Im
|
||
Browser waere es eine zweite Fassung derselben Regel -- und die
|
||
eine wuerde irgendwann etwas anderes sagen als die andere.
|
||
Dieselbe Ueberlegung steht zehn Zeilen tiefer schon einmal, zum
|
||
Termin.
|
||
================================================================= */
|
||
if (bereich === "anschlag") {
|
||
const grenze = Date.now() - ANSCHLAG_STUNDEN * 3600 * 1000;
|
||
for (const e of eintraege) {
|
||
const auf = Date.parse(e.erstellt || "");
|
||
/* OHNE LESBARES DATUM BLEIBT SIE HAENGEN. Ein unlesbarer
|
||
Zeitstempel ist ein Grund nachzusehen, kein Grund, etwas
|
||
verschwinden zu lassen. */
|
||
e.abgehaengt = Number.isFinite(auf) ? auf < grenze : false;
|
||
}
|
||
}
|
||
|
||
/* STEHT DIESER ZETTEL IM KALENDER? (17.09.2026)
|
||
|
||
Der Kalender zeigt seit heute, was auf den Brettern steht und
|
||
einen Termin hat. Die Verbindung war aber EINSEITIG: Vom
|
||
Kalender kam man aufs Brett, vom Brett erfuhr man nichts. Am
|
||
Kartenfuss stand bloss ein Datum -- und "24.09.2026" sieht
|
||
genauso aus wie "17.09.2026", obwohl das eine ein Termin ist
|
||
und das andere der Tag, an dem jemand getippt hat.
|
||
|
||
DIE ANTWORT KOMMT VOM SERVER und wird nicht im Browser
|
||
nachgerechnet: Sonst gaebe es eine zweite Fassung der Regel, und
|
||
die Karte behauptete irgendwann etwas, das der Kalender nicht
|
||
zeigt. Eine Anzeige, die etwas Falsches sagt, ist schlimmer als
|
||
keine -- man glaubt ihr ja.
|
||
|
||
Kostet keine zusaetzliche Abfrage: Alle vier Felder stehen
|
||
ohnehin schon in SPALTEN. */
|
||
{
|
||
const heute = heuteLokal();
|
||
for (const e of eintraege) e.termin = terminVon(e, heute);
|
||
}
|
||
|
||
/* DIE BILDER AN DEN BEITRAEGEN (17.09.2026) -- in EINER Abfrage
|
||
fuer die ganze Liste. Vierzig Beitraege einzeln zu fragen waeren
|
||
vierzig Anfragen, und die Seite ruckelte sichtbar beim Aufbau;
|
||
genau dieselbe Ueberlegung steht schon bei den Rueckmeldungen.
|
||
|
||
WAS HIER MITKOMMT, IST NUR DIE NUMMER. Ob dahinter wirklich ein
|
||
Bild liegt, entscheidet erst der Bildweg selbst -- und zwar an
|
||
den ersten Bytes der Datei, nicht am behaupteten Typ. */
|
||
if (eintraege.length) {
|
||
const platz = eintraege.map(() => "?").join(", ");
|
||
const bilder = db().prepare(
|
||
`SELECT id, eintrag_id, name_original FROM dateien
|
||
WHERE eintrag_id IN (${platz}) ORDER BY id`)
|
||
.all(...eintraege.map((e) => e.id));
|
||
const je = new Map();
|
||
for (const b of bilder) {
|
||
if (!je.has(b.eintrag_id)) je.set(b.eintrag_id, []);
|
||
je.get(b.eintrag_id).push({ id: b.id, name: b.name_original });
|
||
}
|
||
for (const e of eintraege) e.bilder = je.get(e.id) || [];
|
||
|
||
/* DIE KETTE IN BEIDE RICHTUNGEN (17.09.2026). Woher kam dieser
|
||
Eintrag -- und was ist aus ihm geworden? Beides in je EINER
|
||
Abfrage; vierzig Eintraege einzeln zu fragen waeren achtzig
|
||
Anfragen.
|
||
|
||
WER DEN WUNSCH GESCHRIEBEN HAT, kommt mit: Das ist der ganze
|
||
Punkt. Ein Clip, unter dem steht "Euer Wunsch -- von Lena",
|
||
ist etwas anderes als ein Clip. */
|
||
const quellen = eintraege.map((e) => e.aus_eintrag_id).filter(Boolean);
|
||
if (quellen.length) {
|
||
const p2 = quellen.map(() => "?").join(", ");
|
||
const her = new Map(db().prepare(
|
||
`SELECT e.id, e.bereich, e.titel, p.name AS von_name
|
||
FROM eintraege e LEFT JOIN personen p ON p.id = e.erstellt_von
|
||
WHERE e.id IN (${p2})`).all(...quellen).map((z) => [z.id, z]));
|
||
for (const e of eintraege) {
|
||
if (e.aus_eintrag_id) e.aus = her.get(e.aus_eintrag_id) || null;
|
||
}
|
||
}
|
||
const p3 = eintraege.map(() => "?").join(", ");
|
||
/* `quelle_url` kommt mit, damit die Karte unterscheiden kann:
|
||
Aus dem Wunsch ist ein Highlight geworden -- hat es auch schon
|
||
ein Video? Sonst waere der Knopf „Video dazu" entweder immer da
|
||
(auch wenn es laengst eins gibt) oder nie (sobald irgendein
|
||
Highlight daraus entstanden ist). */
|
||
const draus = db().prepare(
|
||
`SELECT id, bereich, titel, aus_eintrag_id, quelle_url FROM eintraege
|
||
WHERE aus_eintrag_id IN (${p3})`).all(...eintraege.map((e) => e.id));
|
||
const jeQuelle = new Map();
|
||
for (const z of draus) {
|
||
if (!jeQuelle.has(z.aus_eintrag_id)) jeQuelle.set(z.aus_eintrag_id, []);
|
||
jeQuelle.get(z.aus_eintrag_id).push({
|
||
id: z.id, bereich: z.bereich, titel: z.titel,
|
||
/* Nur ja/nein -- die Adresse selbst gehoert nicht in eine
|
||
Liste, die nur „was wurde daraus" beantworten soll. */
|
||
hat_video: !!z.quelle_url,
|
||
});
|
||
}
|
||
for (const e of eintraege) e.wurde = jeQuelle.get(e.id) || [];
|
||
|
||
/* DIE ANTWORTEN, in EINER Abfrage fuer die ganze Liste. */
|
||
if (MIT_ANTWORTEN.has(bereich)) {
|
||
const p4 = eintraege.map(() => "?").join(", ");
|
||
const alle = db().prepare(
|
||
`SELECT a.id, a.eintrag_id, a.text, a.erstellt, a.person_id,
|
||
p.name AS von_name, p.rolle AS von_rolle
|
||
FROM eintrag_antworten a
|
||
LEFT JOIN personen p ON p.id = a.person_id
|
||
WHERE a.eintrag_id IN (${p4})
|
||
ORDER BY a.id`).all(...eintraege.map((e) => e.id));
|
||
const jeE = new Map();
|
||
for (const a of alle) {
|
||
if (!jeE.has(a.eintrag_id)) jeE.set(a.eintrag_id, []);
|
||
/* DEN ROLLENNAMEN MACHT `mitRollenname`, wie ueberall im
|
||
Treff. Eine eigene Uebersetzung hier waere eine zweite
|
||
Wahrheit ueber dieselbe Sache -- und Rollennamen sind in
|
||
diesem Haus die Sorte Angabe, bei der zwei Stellen einmal
|
||
zu viel sind. */
|
||
jeE.get(a.eintrag_id).push(mitRollenname({
|
||
id: a.id, text: a.text, erstellt: a.erstellt,
|
||
von_name: a.von_name, von_rolle: a.von_rolle,
|
||
meine: a.person_id === (req.sicht || req.person)?.id,
|
||
}));
|
||
}
|
||
for (const e of eintraege) e.antworten = jeE.get(e.id) || [];
|
||
}
|
||
|
||
/* ==== DIE PUNKTE EINES EVENTS (03.10.2026) ====================
|
||
|
||
Aus dem Fliesstext in event_aufgaben/event_regeln wird eine
|
||
Liste, und an die Aufgaben kommen die Haken der eigenen
|
||
Person. Zerlegt wird HIER und nicht im Browser: Der Schluessel
|
||
eines Punktes entscheidet, wo ein Haken landet -- eine zweite
|
||
Rechenvorschrift dafuer waere die, die beim naechsten Umbau
|
||
auslaeuft, und zwar still.
|
||
|
||
IN EINER ABFRAGE FUER DIE GANZE LISTE, wie bei den Bildern und
|
||
den Antworten darueber. Ein Event hat ein Dutzend Punkte und
|
||
ein Brett ein Dutzend Events; einzeln gefragt waere das bei
|
||
jedem Seitenaufbau ein Schwall kleiner Abfragen.
|
||
|
||
NUR DORT, WO ES EVENTS GIBT. Auf den uebrigen Brettern sind
|
||
diese Felder leer, und eine Abfrage mit leerer Liste ist eine
|
||
Abfrage ohne Zweck. */
|
||
const events = eintraege.filter((e) => e.event_aufgaben || e.event_regeln);
|
||
if (events.length) {
|
||
const ichId = (req.sicht || req.person)?.id;
|
||
const p5 = events.map(() => "?").join(", ");
|
||
/* Meine eigenen Haken ... */
|
||
const meine = new Map();
|
||
for (const z of db().prepare(
|
||
`SELECT eintrag_id, punkt FROM event_punkte
|
||
WHERE eintrag_id IN (${p5}) AND person_id = ?`)
|
||
.all(...events.map((e) => e.id), ichId)) {
|
||
if (!meine.has(z.eintrag_id)) meine.set(z.eintrag_id, new Set());
|
||
meine.get(z.eintrag_id).add(z.punkt);
|
||
}
|
||
|
||
/* ... und fuer die Leitung: wer macht mit, und wie weit?
|
||
|
||
NUR FUER DIE LEITUNG. „Wer hat was geschafft" ist eine
|
||
Auskunft ueber andere Personen; die gehoert nicht an jede
|
||
Karte, die irgendwer aufmacht. Wer nicht zur Leitung gehoert,
|
||
bekommt das Feld gar nicht erst -- nicht leer, sondern
|
||
ueberhaupt nicht: Ein leeres Feld laedt dazu ein, es spaeter
|
||
„versehentlich" zu fuellen. */
|
||
const stand = new Map();
|
||
if (istLeitung(req.person)) {
|
||
for (const z of db().prepare(
|
||
`SELECT ep.eintrag_id, ep.person_id, p.name, COUNT(*) AS n
|
||
FROM event_punkte ep
|
||
LEFT JOIN personen p ON p.id = ep.person_id
|
||
WHERE ep.eintrag_id IN (${p5})
|
||
GROUP BY ep.eintrag_id, ep.person_id
|
||
ORDER BY n DESC, p.name`)
|
||
.all(...events.map((e) => e.id))) {
|
||
if (!stand.has(z.eintrag_id)) stand.set(z.eintrag_id, []);
|
||
stand.get(z.eintrag_id).push({ id: z.person_id, name: z.name, geschafft: z.n });
|
||
}
|
||
}
|
||
|
||
for (const e of events) {
|
||
const haken = meine.get(e.id) || new Set();
|
||
e.event_punkte = punkteAusText(e.event_aufgaben)
|
||
.map((p) => ({ ...p, meiner: haken.has(p.schluessel) }));
|
||
e.event_regelpunkte = punkteAusText(e.event_regeln);
|
||
/* DIE ZAHL KOMMT MIT, statt im Browser gezaehlt zu werden --
|
||
sie steht sonst an zwei Stellen, und „2 von 3" ist genau
|
||
die Sorte Angabe, die man nicht zweimal haben will. */
|
||
e.event_geschafft = e.event_punkte.filter((p) => p.meiner).length;
|
||
if (stand.size) e.event_stand = stand.get(e.id) || [];
|
||
}
|
||
}
|
||
}
|
||
|
||
/* DASS DIES EIN BRETT DES TREFFS IST, SAGT DER SERVER (11.09.2026).
|
||
|
||
Die Oberflaeche braucht die Auskunft, um ihre Knoepfe zu bauen
|
||
("Will ich auch", anheften, freigeben, melden). Sie koennte den
|
||
Bereichsnamen gegen eine Liste im Browser halten -- dann stuenden
|
||
die Namen aller sieben Bretter in einer ausgelieferten Datei, und
|
||
es gaebe eine zweite Wahrheit darueber, welche es sind. So gibt
|
||
es weiterhin genau eine (TREFF_BRETTER), und der Browser fragt
|
||
sie, statt sie zu kennen. */
|
||
/* WEM GEHOERT EIN EINTRAG AUF DIESEM BRETT? (11.09.2026)
|
||
|
||
Filipe, mit Bildschirmfoto des Feldes "Gilt für: Agentur":
|
||
"auf dieser seite soll nichts stehen von agentur, diese seite
|
||
hier ist nur für mich meine modis und meine community."
|
||
|
||
Er hatte recht, und der Fehler war ein fest eingetragenes Wort:
|
||
In bereich.js stand woertlich "Agentur" -- richtig auf der
|
||
Agenturadresse, falsch auf jeder anderen. Ein Wort in der
|
||
Oberflaeche, das eine Tatsache ueber den Server behauptet, ist
|
||
genau die Sorte Zweitschrift, die auseinanderlaeuft.
|
||
|
||
Jetzt sagt der Server es. Er weiss beides: welches Brett es ist
|
||
und in welchem Haus die Person gerade steht. */
|
||
const gehoert = treffBrett ? "Das Rudel"
|
||
: (req.sicht || req.person)?.haus === "crew" ? "Team Dogi"
|
||
: "Agentur";
|
||
|
||
/* DER NAECHSTE SCHRITT -- vom Server, nicht vom Browser (17.09.2026).
|
||
|
||
Auf dem Brett "Mitmachen" steht, WIE man dazugehoeren kann. Was
|
||
fehlte, war die Stelle, an der man es dann auch tut: Filipe,
|
||
mit dem Bildschirmfoto dieses Bretts, "ich will dass die leute
|
||
sich da anonym bei mir melden koennen".
|
||
|
||
WARUM DER SERVER DEN KNOPF ENTSCHEIDET und nicht bereich.js:
|
||
Weil die Frage "darf diese Person?" an der Rolle haengt, und
|
||
Rollennamen stehen in keiner Datei, die jeder herunterlaedt --
|
||
bereich.js kann ohne Anmeldung aus dem offenen Netz gelesen
|
||
werden (gemessen am 17.09.). Der Browser bekommt hier einen
|
||
fertigen Satz oder gar nichts.
|
||
|
||
UND EIN KNOPF, DEN DER SERVER ABLEHNT, WAERE SCHLIMMER ALS
|
||
KEINER. Wer im Team ist, sieht ihn deshalb nicht. */
|
||
const weiter = (bereich === "mitmachen"
|
||
&& BEWERBUNG_ROLLEN.has((req.sicht || req.person)?.rolle))
|
||
? {
|
||
titel: "Du willst Modi werden?",
|
||
text: "Ein paar Fragen, ehrlich beantwortet. Es liest niemand "
|
||
+ "ausser DogFather und der rechten Hand – kein anderes "
|
||
+ "Mitglied, kein Modi.",
|
||
knopf: "Anfrage stellen",
|
||
ziel: "bewerben.html",
|
||
}
|
||
: null;
|
||
|
||
/* ==== DIE OBERZEILE KOMMT AUS DER KACHEL (20.09.2026) ==========
|
||
|
||
Ueber jedem Brett steht eine kleine Zeile, die sagt, wozu es
|
||
gehoert. Sie stand FEST im HTML: `<p class="marke">Betreuung</p>`
|
||
-- ueberschrieben nur dann, wenn das Brett ein eigenes `ober`
|
||
hat.
|
||
|
||
Gemessen: SECHS Bretter haben keines (live, content, technik,
|
||
community, schutz, agentur). Ueber dreien davon -- „Rund ums
|
||
Live" eines Modi -- stand damit dauerhaft „Betreuung". Ein Modi
|
||
betreut niemanden.
|
||
|
||
Jetzt faellt sie auf die GRUPPE der Kachel zurueck, mit der
|
||
dieser Mensch hierhergekommen ist. Das ist die ehrlichste
|
||
Auskunft, die es gibt: Genau dort hat er geklickt. Und sie ist
|
||
je Rolle richtig, ohne dass jemand eine zweite Liste pflegt --
|
||
dasselbe Brett kann fuer einen Modi in einer anderen Gruppe
|
||
stehen als fuer die rechte Hand. */
|
||
const meine = bereicheFuer(req.sicht || req.person) || [];
|
||
const passend = meine.find((k) => String(k.ziel || "").split("?")[1] === "b=" + bereich);
|
||
const ober = einstellung.ober || passend?.gruppe || null;
|
||
|
||
/* WER ENTSCHEIDET, STEHT AUF DEM BRETT SELBST (21.09.2026).
|
||
|
||
Filipe: "immer wenn es um was geht was ich machen soll ... dass
|
||
ich immer am ende entscheide ob ich was mache oder nicht. ich
|
||
kann immer annehmen oder ablehnen. über all auf der website soll
|
||
dass bei den vorschlägen erwähnt werden."
|
||
|
||
DIE VORSCHLAGSKARTEN SIEHT NUR DAS TEAM -- nachgemessen: Die
|
||
Community bekommt sie mit 404 gar nicht. Sie liest nur, was
|
||
daraus auf dem Brett landet: "Was soll DogFather im naechsten
|
||
Stream unbedingt machen?" Genau dort muss der Satz also stehen,
|
||
nicht nur ueber den Karten.
|
||
|
||
WELCHE BRETTER IHN BEKOMMEN, WIRD ABGELEITET: genau die, fuer
|
||
die es ueberhaupt Vorschlaege gibt (`STARTKATALOG`). Eine
|
||
zweite Liste "Bretter mit Hinweis" waere die naechste, die
|
||
altert -- und wer ein Brett in den Katalog aufnimmt, denkt nicht
|
||
daran, es auch dort einzutragen.
|
||
|
||
`null`, wo es keine Vorschlaege gibt: Auf einem Brett fuer
|
||
Termine oder Dateien waere der Satz sinnlos, und ein Hinweis,
|
||
der ueberall steht, wird nirgends gelesen. */
|
||
const entscheidung = STARTKATALOG[bereich] ? VORSCHLAG_HINWEIS : null;
|
||
|
||
res.json({
|
||
bereich, einstellung: { ...einstellung, ober, entscheidung },
|
||
treff: treffBrett, gehoert, weiter,
|
||
/* WIE LANGE EINE ANSAGE HAENGT -- vom Server, damit die Anzeige
|
||
(„seit gestern abgehaengt") und die Regel dieselbe Zahl
|
||
benutzen. Nur am Anschlagbrett; ueberall sonst gibt es keine
|
||
Frist, und ein Feld, das immer 24 sagt, wuerde irgendwann
|
||
irgendwo ausgewertet. */
|
||
...(bereich === "anschlag" ? { anschlag_stunden: ANSCHLAG_STUNDEN } : {}),
|
||
/* DARF DIESER MENSCH IN DEN KALENDER? Beantwortet von den zwei
|
||
Stellen, die es entscheiden -- nicht von einer dritten Regel
|
||
im Browser. Die Karte zeigt ihren Termin dann als Text statt
|
||
als Link; die Auskunft bleibt, nur der Weg ins Leere faellt
|
||
weg. */
|
||
kalender_offen: darfSeite(req.person, "/workspace/kalender.html")
|
||
&& gehoertAufDieseAdresse(req.person, "/workspace/kalender.html"),
|
||
/* DIE KANAELE KOMMEN VOM SERVER (20.09.2026).
|
||
|
||
Filipe: "die highlights sollen in 3 kategorien sein
|
||
nebeneinander, dogfather, hasidog und dogfather clips."
|
||
|
||
Der Browser hatte dafuer bisher eine eigene kleine Liste
|
||
(`KANAL_WORT` in bereich.js) -- in anderer Reihenfolge und
|
||
ohne die Handles. Eine zweite Fassung derselben Tatsache ist
|
||
die, die beim vierten Account vergessen wird; genau davor
|
||
warnt der Kommentar an KANAELE selbst. Deshalb kommt sie von
|
||
hier, samt Reihenfolge: DogFather, HasiDog, Clips.
|
||
|
||
`kanaeleFuer` und nicht `KANAELE`: Wer nicht zum Team gehoert,
|
||
hat mit den Accounts nichts zu tun und bekommt die Liste gar
|
||
nicht erst. */
|
||
kanaele: kanaeleFuer(req.person),
|
||
eintraege: treffBrett ? eintraege.map(mitRollenname) : eintraege,
|
||
});
|
||
} catch (fehler) {
|
||
console.error("[workspace] Bereich lesen:", fehler?.message);
|
||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|
||
|
||
/* =====================================================================
|
||
EINEN EINTRAG AN DIE PERSON SCHICKEN (11.09.2026)
|
||
|
||
Der Entwicklungs-Bereich ist eine Aufzeichnung fuer DogFather und die
|
||
rechte Hand; die Person selbst sieht ihn nicht. Diese Route ist die
|
||
einzige Bruecke -- und sie geht nur in EINE Richtung, ausdruecklich
|
||
und einzeln.
|
||
|
||
WARUM NICHT EINFACH ALLES SICHTBAR MACHEN: Eine halb sichtbare Akte
|
||
ist schlimmer als eine geschlossene. Niemand weiss mehr, was der
|
||
andere gerade liest, und man schreibt anders -- vorsichtiger,
|
||
weniger, oder gar nicht mehr.
|
||
|
||
WARUM ES DIESE BRUECKE TROTZDEM BRAUCHT: Die Forschung zu
|
||
freiwilligen Moderatoren ist an dieser Stelle eindeutig. Was Menschen
|
||
haelt, ist Anerkennung -- Dank und Rueckmeldung erhoehen die
|
||
Verweildauer messbar. Ein "Das laeuft gut", das nur in einer Akte
|
||
steht, hat niemandem geholfen.
|
||
|
||
DER WEG IST DER CHAT und keine neue Benachrichtigungsart: Die Person
|
||
findet die Nachricht dort, wo sie alles andere auch findet, kann
|
||
antworten, und sie steht in ihrem Verlauf. Ein eigener Kanal dafuer
|
||
waere ein zweiter Ort mit einer roten Zahl.
|
||
|
||
EINMAL IST EINMAL. `gesendet_am` haelt fest, wann es geschehen ist --
|
||
die Route schickt danach nicht noch einmal. Zweimal dasselbe Lob ist
|
||
kein doppeltes Lob, sondern ein Versehen, das jeder bemerkt.
|
||
===================================================================== */
|
||
bereicheRouter.post("/workspace/api/bereich/entwicklung/:id/senden",
|
||
gleicheHerkunft, (req, res) => {
|
||
try {
|
||
if (!fuehrtTeamDogi(req.person)) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
const id = Number(req.params.id);
|
||
if (!Number.isInteger(id)) return res.status(400).json({ fehler: "ungueltig" });
|
||
|
||
const e = db().prepare(
|
||
`SELECT id, bereich, art, titel, text, einsatz, creator_id, gesendet_am
|
||
FROM eintraege WHERE id = ?`).get(id);
|
||
if (!e || e.bereich !== "entwicklung") {
|
||
return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
}
|
||
if (!e.creator_id) {
|
||
return res.status(400).json({
|
||
fehler: "Dieser Eintrag gehört zu niemandem – trag zuerst ein, um wen es geht.",
|
||
});
|
||
}
|
||
if (e.gesendet_am) {
|
||
return res.status(409).json({ fehler: "Das ist schon geschickt.", am: e.gesendet_am });
|
||
}
|
||
|
||
/* DER TEXT WIRD GEBAUT UND NICHT ROH GESCHICKT. Im Bereich steht
|
||
eine Notiz ("Ruhig geblieben, als es kippte"); als Nachricht
|
||
ohne Zusammenhang liest sich das wie ein Fetzen. Die Art davor
|
||
sagt, was gemeint ist, und der Einsatz dahinter, was daraus
|
||
folgen soll -- beides steht ohnehin schon da. */
|
||
const art = BEREICHE.entwicklung.arten[e.art] || "";
|
||
const stuecke = [
|
||
art ? `${art}: ${e.titel}` : e.titel,
|
||
e.text || "",
|
||
e.einsatz ? `Was daraus folgen soll: ${e.einsatz}` : "",
|
||
].filter(Boolean);
|
||
|
||
const geschickt = nachrichtSchicken(req.person, e.creator_id, stuecke.join("\n\n"));
|
||
if (!geschickt) {
|
||
return res.status(400).json({
|
||
fehler: "Die Nachricht ging nicht raus – ist die Person noch aktiv?",
|
||
});
|
||
}
|
||
|
||
const am = jetzt();
|
||
db().prepare("UPDATE eintraege SET gesendet_am = ? WHERE id = ?").run(am, id);
|
||
protokolliere("entwicklung_geschickt", {
|
||
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
||
detail: `#${id} an #${e.creator_id}`,
|
||
});
|
||
res.json({ ok: true, am, raum_id: geschickt.raum_id });
|
||
} catch (fehler) {
|
||
console.error("[workspace] Entwicklung schicken:", fehler?.message);
|
||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|
||
|
||
/* ---------- Prüfen ------------------------------------------------------- */
|
||
|
||
function pruefe(bereich, körper, { neu }) {
|
||
const einstellung = BEREICHE[bereich];
|
||
const fehler = [];
|
||
const aus = {};
|
||
|
||
if (neu || körper.titel !== undefined) {
|
||
const t = String(körper.titel ?? "").trim();
|
||
if (t.length < 2) fehler.push("Die Überschrift fehlt.");
|
||
else if (t.length > TITEL_MAX) fehler.push("Titel ist zu lang.");
|
||
else aus.titel = t;
|
||
}
|
||
if (neu || körper.art !== undefined) {
|
||
const a = String(körper.art ?? "");
|
||
if (!Object.hasOwn(einstellung.arten, a)) fehler.push("Unbekannte Art.");
|
||
else aus.art = a;
|
||
}
|
||
if (körper.text !== undefined) {
|
||
const t = String(körper.text ?? "").trim();
|
||
if (t.length > TEXT_MAX) fehler.push("Text ist zu lang.");
|
||
else aus.text = t || null;
|
||
}
|
||
if (neu || körper.datum !== undefined) {
|
||
const d = String(körper.datum ?? "").trim();
|
||
/* ORTSZEIT, NICHT UTC (01.10.2026). Hier stand
|
||
`jetzt().slice(0, 10)` -- der UTC-Tag. Zwischen 00:00 und 02:00
|
||
Ortszeit ist das der VORTAG: Ein Eintrag ohne Datum, nachts
|
||
angelegt, trug gestern. Bei „Was ansteht" landet er damit im
|
||
Abschnitt „Vorbei", und der ist von sich aus zugeklappt -- der
|
||
Eintrag ist also da und trotzdem nicht zu sehen. */
|
||
if (!d) aus.datum = heuteLokal();
|
||
else if (!/^\d{4}-\d{2}-\d{2}$/.test(d) || Number.isNaN(Date.parse(d))) {
|
||
fehler.push("Datum ist ungültig.");
|
||
} else aus.datum = d;
|
||
}
|
||
/* DIE UHRZEIT (17.09.2026) -- optional, und leer heisst ausdruecklich
|
||
"ohne Uhrzeit" und nicht "unveraendert". Sonst liesse sich eine
|
||
einmal gesetzte Zeit nie wieder entfernen, und das faellt erst auf,
|
||
wenn ein Termin verschoben wird. */
|
||
if (körper.uhrzeit !== undefined) {
|
||
const u = String(körper.uhrzeit ?? "").trim();
|
||
if (!u) aus.uhrzeit = null;
|
||
else if (!/^([01]\d|2[0-3]):[0-5]\d$/.test(u)) fehler.push("Uhrzeit ist ungültig.");
|
||
else aus.uhrzeit = u;
|
||
}
|
||
if (körper.status !== undefined) {
|
||
/* JE BEREICH, NICHT GLOBAL (17.09.2026). Die Wunschliste kennt
|
||
vier Zustaende, alle anderen zwei. Eine gemeinsame Liste haette
|
||
"Diesmal nicht" auch an einem Schutzvorfall erlaubt -- und dort
|
||
bedeutet es nichts. */
|
||
const erlaubt = einstellung.zustaende
|
||
? Object.keys(einstellung.zustaende) : STATUS;
|
||
if (!erlaubt.includes(körper.status)) fehler.push("Unbekannter Status.");
|
||
else aus.status = körper.status;
|
||
}
|
||
/* DER GEPLANTE TERMIN eines Wunsches. Bisher gab es das Feld nur in
|
||
der Content-Planung; hier traegt es dieselbe Bedeutung und darf
|
||
deshalb dasselbe Format haben. Leer heisst "noch kein Termin" und
|
||
nicht "unveraendert". */
|
||
if (einstellung.mitGeplant && körper.geplant !== undefined) {
|
||
const g = String(körper.geplant ?? "").trim();
|
||
if (!g) aus.geplant = null;
|
||
else if (!/^\d{4}-\d{2}-\d{2}$/.test(g) || Number.isNaN(Date.parse(g))) {
|
||
fehler.push("Der geplante Tag ist ungültig.");
|
||
} else aus.geplant = g;
|
||
}
|
||
/* Bewertung und Dringlichkeit gibt es nur dort, wo der Bereich sie
|
||
vorsieht -- sonst werden sie stillschweigend verworfen. */
|
||
if (einstellung.bewertung && körper.bewertung !== undefined) {
|
||
if (körper.bewertung === null || körper.bewertung === "") aus.bewertung = null;
|
||
else {
|
||
const b = Number(körper.bewertung);
|
||
if (!Number.isInteger(b) || b < 1 || b > 5) fehler.push("Bewertung muss 1 bis 5 sein.");
|
||
else aus.bewertung = b;
|
||
}
|
||
}
|
||
if (einstellung.dringlichkeit && körper.dringlichkeit !== undefined) {
|
||
if (!DRINGLICHKEITEN.includes(körper.dringlichkeit)) fehler.push("Unbekannte Dringlichkeit.");
|
||
else aus.dringlichkeit = körper.dringlichkeit;
|
||
}
|
||
/* Nur dort, wo der Bereich es vorsieht -- sonst koennte man einen
|
||
Schutzvorfall oder eine Content-Idee als "vertraulich" markieren
|
||
und damit vor den eigenen Leuten verstecken. */
|
||
if (einstellung.vertraulich && körper.nur_leitung !== undefined) {
|
||
aus.nur_leitung = (körper.nur_leitung === true || körper.nur_leitung === 1
|
||
|| körper.nur_leitung === "1") ? 1 : 0;
|
||
}
|
||
|
||
/* Die vier Content-Felder gibt es nur in der Content-Planung. In jedem
|
||
anderen Bereich werden sie stillschweigend verworfen -- wie Bewertung
|
||
und Dringlichkeit auch. Sonst stuende irgendwann ein Aufhaenger an
|
||
einem Schutzvorfall. */
|
||
if (bereich === "content") {
|
||
if (körper.hook !== undefined) {
|
||
const h = String(körper.hook ?? "").trim();
|
||
if (h.length > 300) fehler.push("Der Hook ist zu lang.");
|
||
else aus.hook = h || null;
|
||
}
|
||
if (körper.format !== undefined) {
|
||
const f = String(körper.format ?? "").trim();
|
||
if (f.length > 60) fehler.push("Das Format ist zu lang.");
|
||
else aus.format = f || null;
|
||
}
|
||
if (körper.saeule_id !== undefined) {
|
||
if (körper.saeule_id === null || körper.saeule_id === "") aus.saeule_id = null;
|
||
else {
|
||
const s = Number(körper.saeule_id);
|
||
if (!Number.isInteger(s) || s < 1) fehler.push("Unbekannte Säule.");
|
||
else aus.saeule_id = s;
|
||
}
|
||
}
|
||
if (körper.geplant !== undefined) {
|
||
const g = String(körper.geplant ?? "").trim();
|
||
if (!g) aus.geplant = null;
|
||
/* Datum ODER Datum mit Uhrzeit -- wer nur den Tag weiss, soll nicht
|
||
zu einer erfundenen Uhrzeit gezwungen werden. */
|
||
else if (!/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2})?$/.test(g) || Number.isNaN(Date.parse(g))) {
|
||
fehler.push("Der geplante Zeitpunkt ist ungültig.");
|
||
} else aus.geplant = g;
|
||
}
|
||
}
|
||
/* ---- Agentur-Events: Zeitraum, Aufgaben, Regeln -------------------
|
||
Nur im Agentur-Bereich, nach demselben Muster wie die vier
|
||
Content-Felder darueber: anderswo werden sie stillschweigend
|
||
verworfen. Sonst haenge eines Tages ein Preisausschreiben an einem
|
||
Schutzvorfall. */
|
||
if (bereich === "agentur") {
|
||
if (körper.event_ende !== undefined) {
|
||
const e = String(körper.event_ende ?? "").trim();
|
||
if (!e) aus.event_ende = null;
|
||
else if (!/^\d{4}-\d{2}-\d{2}$/.test(e) || Number.isNaN(Date.parse(e))) {
|
||
fehler.push("Das Ende des Zeitraums ist ungültig.");
|
||
} else aus.event_ende = e;
|
||
}
|
||
for (const [feld, name] of [["event_aufgaben", "Die Aufgaben"], ["event_regeln", "Die Regeln"]]) {
|
||
if (körper[feld] === undefined) continue;
|
||
const t = String(körper[feld] ?? "").trim();
|
||
if (t.length > TEXT_MAX) fehler.push(`${name} sind zu lang.`);
|
||
else aus[feld] = t || null;
|
||
}
|
||
/* "Bis" vor "von" ist kein Zeitraum, sondern ein Tippfehler -- und
|
||
einer, den man einer Liste nicht ansieht: Der Eintrag stuende da,
|
||
liefe aber schon abgelaufen ein. Geprueft wird gegen das Datum,
|
||
das GERADE gesetzt wird; beim Aendern nur einer der beiden Seiten
|
||
ergaenzt der Aufrufer die andere mit (siehe PATCH). */
|
||
if (aus.event_ende && aus.datum && aus.event_ende < aus.datum) {
|
||
fehler.push("Das Ende liegt vor dem Beginn.");
|
||
}
|
||
}
|
||
|
||
if (körper.creator_id !== undefined) {
|
||
const w = körper.creator_id;
|
||
if (w === null || w === "") aus.creator_id = null;
|
||
else {
|
||
const z = Number(w);
|
||
if (!Number.isInteger(z) || z < 1) fehler.push("Ungültige Zuordnung.");
|
||
else aus.creator_id = z;
|
||
}
|
||
}
|
||
|
||
/* Ein frei eingetragener Name statt eines Creators -- eine Marke, ein
|
||
Partner, jemand ohne Konto im Workspace. Er schlaegt die Auswahl:
|
||
Wer tippt, meint das Getippte. Der Eintrag gehoert dann zu keinem
|
||
Konto; sichtbar bleibt er ueber erstellt_von (siehe sichtbar()). */
|
||
externPruefen(körper, aus, "creator", fehler);
|
||
|
||
/* Das Einsatz-Feld gibt es nur in Bereichen, die es kennen. Ein
|
||
Aufruf an der Oberflaeche vorbei soll nicht in jedem Bereich eine
|
||
Spalte fuellen koennen, die dort nichts bedeutet. */
|
||
if (einstellung.einsatzFeld && körper.einsatz !== undefined) {
|
||
const t = String(körper.einsatz ?? "").trim();
|
||
if (t.length > TEXT_MAX) fehler.push("Der Einsatz ist zu lang.");
|
||
else aus.einsatz = t || null;
|
||
}
|
||
|
||
/* ---- ZU WELCHEM ACCOUNT? (20.09.2026) ---------------------------
|
||
|
||
Filipe: "und wenn man was hinzufuegt soll man auch den account
|
||
auswaehlen koennen."
|
||
|
||
NUR AUF DEN HIGHLIGHTS und nur gegen die echte Kanalliste geprueft.
|
||
Ein Aufruf an der Oberflaeche vorbei soll keinen erfundenen Kanal
|
||
in die Spalte schreiben koennen -- danach stuende in der Galerie
|
||
eine vierte Spalte, die es nicht gibt, und niemand faende heraus,
|
||
woher sie kommt. Eine Regel, die nur im Formular steht, ist keine
|
||
Regel (siehe der Absatz gleich darunter).
|
||
|
||
KEIN PFLICHTFELD: Leer bedeutet "noch offen" -- ein falscher
|
||
Eintrag ist schlechter als ein leerer, genau wie an KANAELE
|
||
vermerkt. Und wer einen TikTok-Link einfuegt, bekommt den Kanal
|
||
ohnehin aus dem Handle abgeleitet; DAS ist genauer als jede
|
||
Auswahl, weil niemand sich vertippen kann. Diese Zeilen
|
||
ueberschreiben das nicht: Die Video-Route setzt `quelle_kanal`
|
||
selbst und geht hier nicht durch. */
|
||
if (bereich === "highlight" && körper.quelle_kanal !== undefined) {
|
||
const w = String(körper.quelle_kanal ?? "").trim();
|
||
if (!w) aus.quelle_kanal = null;
|
||
else if (KANAELE.some((k) => k.wert === w)) aus.quelle_kanal = w;
|
||
else fehler.push("Diesen Account gibt es nicht.");
|
||
}
|
||
|
||
/* Ein Agentur-Eintrag gehoert NIEMANDEM einzeln (07.09.2026).
|
||
|
||
Er gilt fuer alle, und genau deshalb sieht ihn auch jeder (siehe
|
||
`fuerAlle` oben). Eine Zuordnung waere hier nicht bloss unnoetig,
|
||
sie waere irrefuehrend: Ein Event mit dem Namen einer Creatorin
|
||
daneben liest sich, als sei es ihres. Die Oberflaeche bietet
|
||
deshalb nur noch "Agentur" an -- und diese Zeilen sorgen dafuer,
|
||
dass ein Aufruf an der Oberflaeche vorbei zum selben Ergebnis
|
||
kommt. Eine Regel, die nur im Formular steht, ist keine Regel.
|
||
|
||
GANZ ZUM SCHLUSS, und das ist der Punkt: Der freie Name wird eine
|
||
Zeile darueber gesetzt. Stuende dieser Riegel vorher -- der
|
||
naheliegende Ort, gleich bei der Creator-Zuordnung --, haette
|
||
externPruefen ihn danach wieder aufgemacht, und ein getippter Name
|
||
stuende trotz allem am Event. Zwei richtige Regeln, falsche
|
||
Reihenfolge, kein Fehler zu sehen. */
|
||
if (einstellung.ohneCreatorBezug) {
|
||
aus.creator_id = null;
|
||
aus.creator_extern = null;
|
||
}
|
||
|
||
return { aus, fehler };
|
||
}
|
||
|
||
/* =====================================================================
|
||
DER STARTKATALOG (17.09.2026)
|
||
|
||
Filipe, mit drei Bildschirmfotos leerer Bretter: "ich will fertige
|
||
sachen schon drin wo auch die community und alle anderen benutzen
|
||
koennen". Es stand sogar im Plan (§5.1): "Acht leere Seiten sind
|
||
schlimmer als keine."
|
||
|
||
NICHT AUTOMATISCH BEIM START. Ein Text, den niemand gelesen hat,
|
||
stuende sonst als Ansage des Teams auf einer oeffentlichen Seite.
|
||
"Bleibt fair" klingt harmlos, bis jemand fragt, was das im
|
||
Streitfall heisst -- und dann muss dahinterstehen, WER es gemeint
|
||
hat. Uebernehmen ist eine Entscheidung; automatisch fuellen waere
|
||
eine Behauptung.
|
||
|
||
UND DIE BREMSE GILT HIER NICHT. Sie zaehlt Beitraege pro Stunde und
|
||
wuerde beim Uebernehmen von fuenfzehn Regeln nach dem zehnten
|
||
abbrechen -- mit einer halb gefuellten Seite und der Meldung, man
|
||
habe zu viel geschrieben. Das ist genau die Sorte Sicherung, die das
|
||
richtige Verhalten bestraft und deshalb abgeschaltet wird. Der Weg
|
||
hier ist eine EINZELNE Handlung des Teams, kein Schreibrausch. */
|
||
/* =====================================================================
|
||
VORLAGEN FUER DIE COMMUNITY (18.09.2026)
|
||
|
||
Filipe: „ich will auch dass du fertige und beispiele machst fuer die
|
||
community damit die auch start hilfe haben ueberall."
|
||
|
||
NICHT ZU VERWECHSELN MIT DEM STARTKATALOG darueber. Der ist fuer das
|
||
Team: fertige Beitraege, die jemand uebernimmt, damit kein Brett
|
||
leer dasteht. Hier geht es um das andere Ende -- ein Mitglied steht
|
||
vor einem leeren Feld und weiss nicht, was hineingehoert.
|
||
|
||
NUR WER SCHREIBEN DARF. Ein Mitglied, das die Sieben-Tage-Frist noch
|
||
nicht hinter sich hat, sieht das Formular gar nicht; Vorlagen dazu
|
||
waeren ein Angebot fuer eine Tuer, die zu ist. Dieselbe Regel wie
|
||
beim Startkatalog, nur mit umgekehrtem Vorzeichen: dort „wer
|
||
schreiben darf, bekommt NICHTS", hier „nur wer schreiben darf". */
|
||
bereicheRouter.get("/workspace/api/bereich/:bereich/vorlagen", (req, res) => {
|
||
try {
|
||
const bereich = String(req.params.bereich);
|
||
if (!BEREICHE[bereich]) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
if (darfSchreiben(req.person, bereich)) return res.json({ vorlagen: [] });
|
||
res.json({ vorlagen: vorlagenFuer(bereich) });
|
||
} catch (fehler) {
|
||
console.error("[workspace] Vorlagen:", fehler?.message);
|
||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|
||
|
||
bereicheRouter.get("/workspace/api/bereich/:bereich/start", (req, res) => {
|
||
try {
|
||
const bereich = String(req.params.bereich);
|
||
if (!BEREICHE[bereich]) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
/* Wer hier nicht schreiben darf, bekommt den Katalog gar nicht --
|
||
statt ihn zu bekommen und den Knopf ausgeblendet zu sehen. */
|
||
if (darfSchreiben(req.person, bereich)) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
const liste = STARTKATALOG[bereich] || [];
|
||
/* Was schon dasteht, kommt nicht noch einmal. Verglichen wird ueber
|
||
den Titel: Eine eigene Kennung an der Zeile waere eine zweite
|
||
Wahrheit, und beim Umformulieren liefe sie auseinander. */
|
||
const da = new Set(db().prepare(
|
||
"SELECT titel FROM eintraege WHERE bereich = ?").all(bereich).map((z) => z.titel));
|
||
|
||
/* ==== EIN FENSTER STATT DER GANZEN LISTE (20.09.2026) ===========
|
||
|
||
Filipe: "fertige vorschlaege, ueberall auf der ganzen kompletten
|
||
website, will ich dass man auch immer wieder neuladen kann, so
|
||
dass wenn man alle benutzt hat man neue erstellen kann oder wenn
|
||
keiner uns gefaellt wir komplett neue erstellen koennen
|
||
automatisch ... dass es immer wieder moeglich ist die liste zu
|
||
wechseln wenn sie uns nicht gefaellt oder leer ist."
|
||
|
||
BISHER KAMEN ALLE AUF EINMAL. Damit gab es nichts zu wechseln --
|
||
die Liste war entweder ganz da oder ganz weg. Und bei 17
|
||
Vorschlaegen (Brett "regeln") ist eine Wand aus Karten das
|
||
Gegenteil von "uebernehmen, was passt".
|
||
|
||
JETZT: vier auf einmal, und ein Knopf holt die naechsten vier.
|
||
Ist der Vorrat durch, faengt er von vorn an -- `ab` wird mit
|
||
Rest gerechnet, es gibt also kein Ende und keine Sackgasse.
|
||
|
||
WAS ES NICHT IST: Die Vorschlaege werden nicht ERZEUGT. Sie sind
|
||
ein geschriebener Vorrat (113 Stueck auf 16 Brettern), und das
|
||
steht auch in der Antwort -- `vorrat` sagt, wie viele es
|
||
insgesamt sind. Der Oberflaeche zu erlauben, "neue" zu
|
||
versprechen, die es nicht gibt, waere eine Luege mit
|
||
Fortschrittsanzeige. */
|
||
const FENSTER = 4;
|
||
const offeneListe = liste.map((v, nr) => ({ nr, ...v, schon: da.has(v.titel) }))
|
||
.filter((v) => !v.schon);
|
||
|
||
const roh = Number(req.query?.ab);
|
||
/* MODULO STATT GRENZE: Wer oft genug weiterklickt, landet wieder am
|
||
Anfang -- statt vor einer leeren Liste zu stehen. */
|
||
const ab = offeneListe.length
|
||
? ((Number.isFinite(roh) ? Math.trunc(roh) : 0) % offeneListe.length
|
||
+ offeneListe.length) % offeneListe.length
|
||
: 0;
|
||
|
||
const fenster = [];
|
||
for (let i = 0; i < Math.min(FENSTER, offeneListe.length); i++) {
|
||
fenster.push(offeneListe[(ab + i) % offeneListe.length]);
|
||
}
|
||
|
||
res.json({
|
||
vorschlaege: fenster,
|
||
/* `offen` bleibt, was es war: wie viele es NOCH gibt. Die
|
||
Oberflaeche zaehlt damit "Alle N uebernehmen" richtig. */
|
||
offen: offeneListe.length,
|
||
/* NEU: Gibt es ueberhaupt etwas zu wechseln? Ohne diese Angabe
|
||
muesste die Seite rechnen, und ein Knopf, der nichts anderes
|
||
zeigt, ist ein Knopf, der nichts tut. */
|
||
mehr_da: offeneListe.length > FENSTER,
|
||
ab,
|
||
vorrat: liste.length,
|
||
/* DER HINWEIS GEHT IMMER MIT (21.09.2026). Er kommt aus
|
||
workspace-treff-start.js, damit es ihn im ganzen Haus genau
|
||
einmal gibt -- und damit eine Pruefung ihn gegen die Quelle
|
||
halten kann statt gegen eine Abschrift. */
|
||
hinweis: VORSCHLAG_HINWEIS,
|
||
});
|
||
} catch (fehler) {
|
||
console.error("[workspace] Startkatalog:", fehler?.message);
|
||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|
||
|
||
bereicheRouter.post("/workspace/api/bereich/:bereich/start", gleicheHerkunft, (req, res) => {
|
||
try {
|
||
const bereich = String(req.params.bereich);
|
||
const einstellung = BEREICHE[bereich];
|
||
if (!einstellung) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
const warum = darfSchreiben(req.person, bereich);
|
||
if (warum) return res.status(403).json({ fehler: warum });
|
||
|
||
const liste = STARTKATALOG[bereich] || [];
|
||
if (!liste.length) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
|
||
const nr = req.body?.nr;
|
||
const gewaehlt = nr === undefined || nr === null || nr === ""
|
||
? liste
|
||
: [liste[Number(nr)]].filter(Boolean);
|
||
if (!gewaehlt.length) return res.status(400).json({ fehler: "Unbekannter Vorschlag." });
|
||
|
||
const da = new Set(db().prepare(
|
||
"SELECT titel FROM eintraege WHERE bereich = ?").all(bereich).map((z) => z.titel));
|
||
const neu = gewaehlt.filter((v) => !da.has(v.titel));
|
||
|
||
let angelegt = 0;
|
||
for (const v of neu) {
|
||
/* DIE ART MUSS ZUM BEREICH PASSEN. Sie steht im Katalog, aber
|
||
geprueft wird sie trotzdem -- sonst traegt ein Tippfehler im
|
||
Katalog eine Art in die Datenbank, die es dort nicht gibt, und
|
||
die Zeile laesst sich hinterher nicht mehr aendern. */
|
||
if (!Object.hasOwn(einstellung.arten, v.art)) continue;
|
||
const { lastInsertRowid } = db().prepare(`
|
||
INSERT INTO eintraege (bereich, art, titel, text, datum, status, erstellt,
|
||
erstellt_von, haus)
|
||
VALUES (?,?,?,?,?, 'offen', ?, ?, ?)`)
|
||
.run(bereich, v.art, v.titel, v.text ?? null,
|
||
/* Der TAG in Ortszeit, der ZEITSTEMPEL in UTC -- das ist kein
|
||
Widerspruch: Ein Zeitpunkt ist ueberall derselbe, ein
|
||
Kalendertag nicht. */
|
||
heuteLokal(), jetzt(), req.person.id,
|
||
hausFuerNeuenEintrag(req.person, bereich));
|
||
|
||
/* UND SOFORT FREIGEBEN (17.09.2026) -- der Fehler, den Filipe
|
||
auf seinem Bildschirm gesehen hat.
|
||
|
||
Gemessen, BEVOR etwas gebaut wurde: DogFather uebernimmt bei
|
||
"Was ansteht" vier Vorschlaege und bei den Highlights drei.
|
||
Auf seinem Bildschirm stehen sie. Die Community sah:
|
||
|
||
Brett DogFather rechte Hand Modi Community
|
||
ansteht 4 4 4 0
|
||
highlight 3 3 3 0
|
||
|
||
Beide Bretter verlangen eine Freigabe, bevor jemand von
|
||
aussen etwas sieht -- und ein uebernommener Vorschlag hatte
|
||
keine. Filipe konnte also fuellen, so viel er wollte; fuer die
|
||
Community blieben genau die zwei Bretter leer, auf die es
|
||
ankommt. Und es waere niemandem aufgefallen, weil es auf
|
||
jedem Team-Bildschirm richtig aussieht.
|
||
|
||
WARUM DAS KEINE UMGEHUNG DER FREIGABE IST: Die Freigabe fragt
|
||
"hat das Team diesen Text gesehen?". Bei einem uebernommenen
|
||
Vorschlag lautet die Antwort ja -- jemand aus dem Team hat ihn
|
||
ausgesucht und auf "Uebernehmen" gedrueckt. Das IST die
|
||
Entscheidung. Eine zweite daneben waere keine Sicherheit,
|
||
sondern ein Klick.
|
||
|
||
NUR FUER TEAM DOGI, und die Bedingung fragt die ROLLE, nicht
|
||
den Weg. Duerfte eines Tages jemand von aussen den Katalog
|
||
uebernehmen, ginge sein Eintrag wie jeder andere zuerst durch
|
||
die Freigabe. */
|
||
if (TREFF_FREIGABE_BRETTER.includes(bereich)
|
||
&& TREFF_TEAM_ROLLEN.has(req.person?.rolle)) {
|
||
db().prepare(
|
||
"INSERT OR IGNORE INTO treff_freigaben (eintrag_id, zeitpunkt, von) VALUES (?,?,?)")
|
||
.run(Number(lastInsertRowid), jetzt(), req.person.id);
|
||
}
|
||
angelegt++;
|
||
}
|
||
protokolliere("bereich_start_uebernommen", {
|
||
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
||
detail: `${bereich} ${angelegt} von ${gewaehlt.length}`,
|
||
});
|
||
res.status(201).json({ angelegt, schon: gewaehlt.length - angelegt });
|
||
} catch (fehler) {
|
||
console.error("[workspace] Startkatalog uebernehmen:", fehler?.message);
|
||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|
||
|
||
/* =====================================================================
|
||
DER KREISLAUF (17.09.2026) -- aus dem Plan im Vault, Stufe 8.
|
||
|
||
Bisher waren die acht Bretter acht Silos. Ein Wunsch wurde
|
||
angenommen, irgendwann passierte etwas im Stream, irgendwann lag ein
|
||
Clip bei den Highlights -- und nichts davon wusste voneinander. Wer
|
||
den Wunsch geschrieben hatte, erfuhr nie, dass er erfuellt wurde.
|
||
|
||
Wunsch -> Termin bei "Was ansteht" -> Clip bei "Highlights"
|
||
|
||
DAS IST DER UNTERSCHIED ZWISCHEN EINEM BRIEFKASTEN UND EINER
|
||
COMMUNITY. Jemand schreibt einen Wunsch und sieht ihn vier Wochen
|
||
spaeter als Clip wieder, mit seinem Namen daneben.
|
||
|
||
EINE EIGENE ROUTE UND KEIN FELD AM FORMULAR: Der Zusammenhang
|
||
entsteht durch eine HANDLUNG ("daraus wird ein Termin"), nicht durch
|
||
ein Auswahlfeld, in dem man einen von vierzig Eintraegen sucht. Wer
|
||
ein Formular ausfuellt, denkt nicht an den Wunsch von letzter Woche.
|
||
===================================================================== */
|
||
bereicheRouter.post("/workspace/api/bereich/:bereich/:id/daraus",
|
||
gleicheHerkunft, (req, res) => {
|
||
try {
|
||
const vonBereich = String(req.params.bereich);
|
||
const id = Number(req.params.id);
|
||
const nach = String(req.body?.nach || "");
|
||
if (!BEREICHE[vonBereich] || !BEREICHE[nach] || !Number.isInteger(id)) {
|
||
return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
}
|
||
/* NUR INNERHALB DER COMMUNITY. Eine Kette, die in die
|
||
Content-Planung eines Creators fuehrt, waere ein Leck: Dort
|
||
steht Arbeit, die die Community nichts angeht. */
|
||
if (!TREFF_BRETTER.includes(vonBereich) || !TREFF_BRETTER.includes(nach)) {
|
||
return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
}
|
||
const warum = darfSchreiben(req.person, nach);
|
||
if (warum) return res.status(403).json({ fehler: warum });
|
||
|
||
const regel = sichtbar(req.person);
|
||
if (!regel) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
/* DIE QUELLE MUSS SICHTBAR SEIN. Sonst liesse sich durch
|
||
Ausprobieren herausfinden, welche Nummern es gibt -- und der
|
||
Titel kaeme im neuen Eintrag gleich mit. */
|
||
const quelle = db().prepare(
|
||
"SELECT id, bereich, titel, erstellt_von FROM eintraege WHERE id = ? AND bereich = ?")
|
||
.get(id, vonBereich);
|
||
if (!quelle) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
|
||
/* SCHON GEMACHT? Dann dieselbe Antwort wie beim Startkatalog:
|
||
nichts Neues, kein Fehler. Zwei Termine aus einem Wunsch
|
||
waeren ein Versehen, kein Wunsch. */
|
||
const schon = db().prepare(
|
||
"SELECT id FROM eintraege WHERE aus_eintrag_id = ? AND bereich = ?").get(id, nach);
|
||
if (schon) return res.status(200).json({ id: schon.id, schon: true });
|
||
|
||
const arten = Object.keys(BEREICHE[nach].arten);
|
||
const art = arten.includes(String(req.body?.art)) ? String(req.body.art) : arten[0];
|
||
|
||
const { lastInsertRowid } = db().prepare(`
|
||
INSERT INTO eintraege (bereich, art, titel, text, datum, status,
|
||
erstellt, erstellt_von, aus_eintrag_id, haus)
|
||
VALUES (?,?,?,?,?, 'offen', ?,?,?,?)`)
|
||
.run(nach, art, quelle.titel,
|
||
String(req.body?.text ?? "").trim().slice(0, TEXT_MAX) || null,
|
||
/* DIE STELLE, AN DER ES AUFGEFALLEN IST (01.10.2026).
|
||
pruef-kreislauf legt einen Wunsch mit dem heutigen Datum an
|
||
und macht daraus einen Termin; gemessen um 01:22 kam
|
||
`2026-09-30` heraus statt `2026-10-01`. Der Termin lag
|
||
damit in der Vergangenheit, stand auf dem Brett unter
|
||
„Vorbei" und war zugeklappt -- waehrend beim Wunsch
|
||
„Daraus wurde ein Termin" zu lesen war. */
|
||
heuteLokal(), jetzt(), req.person.id, quelle.id,
|
||
hausFuerNeuenEintrag(req.person, nach));
|
||
|
||
protokolliere("bereich_daraus", {
|
||
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
||
detail: `${vonBereich}#${id} -> ${nach}`,
|
||
});
|
||
res.status(201).json({ id: Number(lastInsertRowid), schon: false });
|
||
} catch (fehler) {
|
||
console.error("[workspace] Kreislauf:", fehler?.message);
|
||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|
||
|
||
/* =====================================================================
|
||
ANTWORTEN (17.09.2026) -- Stufe 6 des Plans
|
||
|
||
"Das Rudel" war eine Liste aus Karten. Der einzige Bereich, in dem
|
||
Menschen MITEINANDER reden sollen, zwang sie, nebeneinander zu
|
||
reden.
|
||
|
||
DIE BREMSE GILT AUCH HIER. Eine Antwort ist ein Beitrag; wer sie
|
||
ausnaehme, haette die Bremse mit drei Klicks umgangen. Sie zaehlt
|
||
allerdings ihren EIGENEN Vorrat: Wer zehn Beitraege geschrieben hat,
|
||
soll noch antworten koennen -- ein Gespraech, das mitten im Satz
|
||
abbricht, ist schlimmer als eins, das nicht angefangen wird. */
|
||
const ANTWORTEN_JE_STUNDE = 20;
|
||
const ANTWORT_MAX = 1200;
|
||
|
||
bereicheRouter.post("/workspace/api/bereich/:bereich/:id/antwort",
|
||
gleicheHerkunft, (req, res) => {
|
||
try {
|
||
const bereich = String(req.params.bereich);
|
||
const id = Number(req.params.id);
|
||
if (!MIT_ANTWORTEN.has(bereich) || !Number.isInteger(id)) {
|
||
return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
}
|
||
const warum = darfSchreiben(req.person, bereich);
|
||
if (warum) return res.status(403).json({ fehler: warum });
|
||
|
||
const text = String(req.body?.text ?? "").trim();
|
||
if (!text) return res.status(400).json({ fehler: "Schreib etwas." });
|
||
if (text.length > ANTWORT_MAX) return res.status(400).json({ fehler: "Das ist zu lang." });
|
||
|
||
const regel = sichtbar(req.person);
|
||
const e = db().prepare("SELECT id FROM eintraege WHERE id = ? AND bereich = ?")
|
||
.get(id, bereich);
|
||
if (!e || !regel) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
|
||
const seit = new Date(Date.now() - 3600_000).toISOString();
|
||
const { anzahl } = db().prepare(
|
||
"SELECT COUNT(*) AS anzahl FROM eintrag_antworten WHERE person_id = ? AND erstellt >= ?")
|
||
.get(req.person.id, seit);
|
||
if (anzahl >= ANTWORTEN_JE_STUNDE) {
|
||
return res.status(429).json({
|
||
fehler: `Du hast in der letzten Stunde ${anzahl} Antworten geschrieben. `
|
||
+ "Mach mal kurz Pause.",
|
||
});
|
||
}
|
||
|
||
const { lastInsertRowid } = db().prepare(
|
||
"INSERT INTO eintrag_antworten (eintrag_id, person_id, text, erstellt) VALUES (?,?,?,?)")
|
||
.run(id, req.person.id, text, jetzt());
|
||
res.status(201).json({ id: Number(lastInsertRowid) });
|
||
} catch (fehler) {
|
||
console.error("[workspace] Antwort:", fehler?.message);
|
||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|
||
|
||
/* DER PFAD LIEGT AUSSERHALB VON `/bereich/` -- und das ist eine
|
||
Reparatur, keine Geschmacksfrage (17.09.2026).
|
||
|
||
Zuerst hiess er `/api/bereich/antwort/:id`. Darueber liegt aber eine
|
||
Middleware auf `/api/bereich/:bereich`, die prueft, ob es den
|
||
Bereich gibt und ob die Person hineindarf -- und "antwort" ist kein
|
||
Bereich. Ergebnis: Fuer DogFather ging es (ein frueherer Zweig liess
|
||
ihn durch), fuer einen Modi kam 404. Ein Weg, der je nach Rolle an
|
||
einer voellig anderen Stelle scheitert, sucht man lange.
|
||
|
||
Gefunden hat es die Pruefung, nicht das Lesen: "das Team loescht
|
||
auch fremde" war gruen, "Lena loescht ihre eigene" nicht. */
|
||
bereicheRouter.delete("/workspace/api/antwort/:id",
|
||
gleicheHerkunft, (req, res) => {
|
||
try {
|
||
const id = Number(req.params.id);
|
||
if (!Number.isInteger(id)) return res.status(400).json({ fehler: "ungueltig" });
|
||
const a = db().prepare(
|
||
"SELECT id, person_id FROM eintrag_antworten WHERE id = ?").get(id);
|
||
if (!a) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
/* DIE EIGENE IMMER, FREMDE NUR DAS TEAM.
|
||
|
||
HIER STAND `!darfSchreiben(req.person, "treff")` -- und das war
|
||
ein Loch. `darfSchreiben` beantwortet "darf diese Person hier
|
||
SCHREIBEN", und das duerfen auch Gaeste ab der Stufe "dabei".
|
||
Ein Gast haette also jede fremde Antwort loeschen koennen.
|
||
|
||
Gefunden hat es die Pruefung, nicht das Lesen: Sie verlangte,
|
||
dass ein Modi eine fremde Antwort NICHT loeschen kann, und
|
||
bekam 200. Die Erwartung war fuer den Modi falsch -- er
|
||
moderiert ja --, aber der Weg dorthin hat das echte Loch
|
||
freigelegt.
|
||
|
||
`TREFF_TEAM_ROLLEN` ist die Liste, die im Haus ohnehin
|
||
entscheidet, wer im Treff moderiert: modi, hand, admin. */
|
||
const darf = a.person_id === req.person.id
|
||
|| TREFF_TEAM_ROLLEN.has(req.person?.rolle);
|
||
if (!darf) return res.status(403).json({ fehler: "nicht_erlaubt" });
|
||
db().prepare("DELETE FROM eintrag_antworten WHERE id = ?").run(id);
|
||
res.json({ ok: true });
|
||
} catch (fehler) {
|
||
console.error("[workspace] Antwort loeschen:", fehler?.message);
|
||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|
||
|
||
/* ---------- Anlegen ------------------------------------------------------ */
|
||
|
||
bereicheRouter.post("/workspace/api/bereich/:bereich", gleicheHerkunft, (req, res) => {
|
||
try {
|
||
const bereich = String(req.params.bereich);
|
||
if (!BEREICHE[bereich]) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
const regel = sichtbar(req.person);
|
||
if (!regel) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
|
||
/* DIE SCHREIBTAFEL DES TREFFS (11.09.2026).
|
||
|
||
Auf welches der sieben Bretter jemand von aussen schreiben darf,
|
||
steht in TREFF_SCHREIBEN -- an einer Stelle, aus der auch die
|
||
Oberflaeche ihre Knoepfe holt (/api/treff/lage). Waeren es zwei
|
||
Stellen, koennte ein Knopf erscheinen, den der Server ablehnt --
|
||
oder, schlimmer, umgekehrt.
|
||
|
||
DER TEXT WIRD DURCHGEREICHT, nicht durch "nicht_erlaubt" ersetzt:
|
||
"Ab Dabei kannst du hier schreiben" beantwortet die Frage, die
|
||
der Mensch davor gerade hat. */
|
||
const warum = darfSchreiben(req.person, bereich);
|
||
if (warum) return res.status(403).json({ fehler: warum });
|
||
|
||
/* DIE BREMSE (17.09.2026) -- aus dem Plan im Vault, Stufe 4.
|
||
|
||
Der Treff hatte Stufen (wer ueberhaupt schreiben darf) und
|
||
Massnahmen (wer nicht mehr darf), aber nichts dazwischen: WIE
|
||
SCHNELL. Ein Stammgast konnte in einer Minute vierzig Beitraege
|
||
absetzen -- aus Aerger, aus Versehen oder weil jemand ein Skript
|
||
laufen laesst.
|
||
|
||
Ein Bereich, der waechst und keine Bremse hat, waechst genau
|
||
einmal. Deshalb steht diese Stufe im Plan VOR Galerie und
|
||
Gespraech: Mehr Sichtbarkeit heisst mehr Angriffsflaeche.
|
||
|
||
KEINE NEUE TABELLE. Die Antwort steht schon in `eintraege` --
|
||
eine Zaehlung ueber die letzte Stunde genuegt. Eine eigene
|
||
Tabelle waere ein zweiter Ort fuer dieselbe Wahrheit und muesste
|
||
zusaetzlich aufgeraeumt werden.
|
||
|
||
UND DIE ABSAGE SAGT, WANN ES WEITERGEHT. "Zu viele Beitraege"
|
||
ohne Zeitangabe laesst jemanden im Minutentakt weiterprobieren
|
||
-- das ist genau die Last, die man verhindern wollte. */
|
||
if (treffBereich(bereich)) {
|
||
const seit = new Date(Date.now() - 3600_000).toISOString();
|
||
/* NUR DIE COMMUNITY-BRETTER ZAEHLEN MIT. Beim ersten Entwurf
|
||
stand hier keine Bereichsgrenze -- dann haette eine Creatorin,
|
||
die zehn Content-Ideen eintraegt, danach im Treff nichts mehr
|
||
schreiben koennen. Eine Bremse, die die falsche Arbeit
|
||
bestraft, wird abgeschaltet. */
|
||
const felder = TREFF_BRETTER.map(() => "?").join(", ");
|
||
const { anzahl, aeltester } = db().prepare(
|
||
`SELECT COUNT(*) AS anzahl, MIN(erstellt) AS aeltester
|
||
FROM eintraege
|
||
WHERE erstellt_von = ? AND erstellt >= ?
|
||
AND bereich IN (${felder})`)
|
||
.get(req.person.id, seit, ...TREFF_BRETTER);
|
||
if (anzahl >= BEITRAEGE_JE_STUNDE) {
|
||
const frei = new Date(Date.parse(aeltester) + 3600_000);
|
||
const min = Math.max(1, Math.ceil((frei.getTime() - Date.now()) / 60000));
|
||
protokolliere("treff_bremse", {
|
||
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
||
detail: `${bereich} ${anzahl} in einer Stunde`,
|
||
});
|
||
return res.status(429).json({
|
||
fehler: `Du hast in der letzten Stunde ${anzahl} Beiträge geschrieben. `
|
||
+ `In ${min} ${min === 1 ? "Minute" : "Minuten"} geht es weiter.`,
|
||
});
|
||
}
|
||
}
|
||
|
||
const { aus, fehler } = pruefe(bereich, req.body || {}, { neu: true });
|
||
if (fehler.length) return res.status(400).json({ fehler: fehler.join(" ") });
|
||
|
||
/* Ein Creator schreibt immer in den eigenen Bereich, egal was im
|
||
Aufruf steht. Ein Scout in den eines Creators, den er betreut --
|
||
niemals in einen fremden und niemals "auf sich selbst", denn ein
|
||
Scout hat gar keinen eigenen Betreuungsbereich. */
|
||
if (req.person.rolle === "creator") {
|
||
aus.creator_id = req.person.id;
|
||
aus.creator_extern = null; // der eigene Bereich gehört ihm, nicht einem Namen
|
||
} else if (req.person.rolle === "scout" && !aus.creator_extern) {
|
||
/* Der freie Name nimmt diesen Zweig ausdrücklich aus: Wer "Marke
|
||
XY" eintippt, will keinen seiner Creator zugeordnet bekommen.
|
||
Ohne diese Ausnahme hätte die Notlösung darunter den getippten
|
||
Namen stillschweigend durch den erstbesten Creator ersetzt --
|
||
der Eintrag stünde dann bei jemandem, der nichts damit zu tun
|
||
hat. Sichtbar bleibt er über erstellt_von. */
|
||
const w = Number(aus.creator_id);
|
||
if (!darfCreator(req.person, w)) {
|
||
const erster = betreuteIds(req.person)[0];
|
||
if (!erster) return res.status(403).json({ fehler: "Dir ist kein Creator zugeteilt." });
|
||
aus.creator_id = erster;
|
||
}
|
||
}
|
||
/* WEM GEHOERT DER EINTRAG? DAS HAENGT AM BEREICH (11.09.2026).
|
||
|
||
Ueberall sonst zeigt `creator_id` auf einen Creator -- die
|
||
Bereichsseiten sind Betreuungsakten. Im Entwicklungs-Bereich
|
||
zeigt sie auf ein Mitglied von Team Dogi: Dort steht, was ueber
|
||
die Zeit an einem Menschen aus dem Team auffaellt.
|
||
|
||
Die Spalte heisst weiterhin creator_id. Das ist Geschichte, kein
|
||
Inhalt -- sie haelt eine Personennummer, und dieselbe
|
||
Ueberlegung steht schon in workspace-checkliste.js.
|
||
|
||
BEIM ERSTEN ANLAUF FEHLTE DAS, und die Meldung war irrefuehrend
|
||
("Zugeordneter Creator existiert nicht" -- die Person existiert
|
||
sehr wohl, sie ist nur kein Creator). Gefunden von der Pruefung,
|
||
nicht vom Auge. */
|
||
/* DIE LINKE HAND GEHOERT DAZU (23.09.2026). Hier stand
|
||
`rolle IN ('hand','modi')` -- eine zweite abgeschriebene Liste
|
||
derselben Sorte wie in workspace-teamlage.js. Ein Eintrag im
|
||
Entwicklungsbereich liess sich der linken Hand nicht zuordnen,
|
||
und die Meldung dazu lautete „Diese Person gehoert nicht zum
|
||
Team." -- ueber jemanden, der sehr wohl dazugehoert.
|
||
|
||
Abgeleitet aus TEAM_DOGI_ROLLEN, der einen Liste des Hauses. */
|
||
const teamListe = [...TEAM_DOGI_ROLLEN].map((r) => `'${r}'`).join(", ");
|
||
const zielRollen = bereich === "entwicklung"
|
||
? `rolle IN (${teamListe})`
|
||
: "rolle = 'creator'";
|
||
if (aus.creator_id
|
||
&& !db().prepare(`SELECT 1 FROM personen WHERE id = ? AND ${zielRollen}`)
|
||
.get(aus.creator_id)) {
|
||
return res.status(400).json({
|
||
fehler: bereich === "entwicklung"
|
||
? "Diese Person gehört nicht zum Team."
|
||
: "Zugeordneter Creator existiert nicht.",
|
||
});
|
||
}
|
||
|
||
/* Eine Saeule gehoert immer GENAU EINEM Creator. Ohne diese Pruefung
|
||
koennte man ein Video an die Saeule eines fremden Creators haengen
|
||
-- und darueber in der Balance-Auswertung Namen sehen, die einen
|
||
nichts angehen. Die Sichtbarkeitsregel oben schuetzt die Eintraege,
|
||
nicht die Verweise zwischen ihnen. */
|
||
const saeuleFehler = saeulePasst(aus.saeule_id, aus.creator_id);
|
||
if (saeuleFehler) return res.status(400).json({ fehler: saeuleFehler });
|
||
|
||
const { lastInsertRowid } = db().prepare(`
|
||
INSERT INTO eintraege
|
||
(bereich, art, titel, text, datum, uhrzeit, bewertung, dringlichkeit, status,
|
||
creator_id, creator_extern, erstellt, erstellt_von, hook, format, saeule_id, geplant,
|
||
event_ende, event_aufgaben, event_regeln, einsatz, nur_leitung, haus)
|
||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(
|
||
bereich, aus.art, aus.titel, aus.text ?? null, aus.datum, aus.uhrzeit ?? null,
|
||
aus.bewertung ?? null, aus.dringlichkeit ?? "mittel", aus.status ?? "offen",
|
||
aus.creator_id ?? null, aus.creator_extern ?? null, jetzt(), req.person.id,
|
||
aus.hook ?? null, aus.format ?? null, aus.saeule_id ?? null, aus.geplant ?? null,
|
||
aus.event_ende ?? null, aus.event_aufgaben ?? null, aus.event_regeln ?? null,
|
||
aus.einsatz ?? null,
|
||
/* 0 und nicht NULL als Vorgabe: NULL hiesse "unbekannt", und
|
||
unbekannt ist bei einer Vertraulichkeit die falsche Antwort.
|
||
Wer nichts waehlt, schreibt fuers Team -- das ist der offene
|
||
Fall, und der soll der Normalfall sein. */
|
||
aus.nur_leitung ?? 0,
|
||
/* Das Haus aus der Adresse -- letzte Spalte, letzter Wert. */
|
||
hausFuerNeuenEintrag(req.person, bereich));
|
||
|
||
/* DASSELBE VON HAND WIE ÜBER DEN VIDEOWEG (30.09.2026). Wer ein
|
||
Highlight selbst anlegt und es auch freigeben dürfte, braucht
|
||
dafür keinen zweiten Klick. Termine (`ansteht`) bleiben
|
||
unberührt — dort ist die Freigabe der Schalter „Im Treff
|
||
zeigen" und damit eine Entscheidung je Termin. */
|
||
sofortFreigeben(bereich, req.person, lastInsertRowid);
|
||
|
||
protokolliere("eintrag_angelegt", {
|
||
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
||
detail: `${bereich} #${lastInsertRowid} ${aus.titel}`.slice(0, 120),
|
||
});
|
||
res.status(201).json({ id: Number(lastInsertRowid) });
|
||
} catch (fehler) {
|
||
console.error("[workspace] Eintrag anlegen:", fehler?.message);
|
||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|
||
|
||
/* ---------- Ändern und Löschen ------------------------------------------- */
|
||
|
||
/** Darf diese Person diesen Eintrag sehen?
|
||
*
|
||
* ENTSTANDEN AM 03.10.2026, weil das Banner eines Agentur-Events fuer
|
||
* Creator mit 404 beantwortet wurde -- gemessen, nicht vermutet:
|
||
*
|
||
* workspace.dogfather-universe.com admin Bild=200
|
||
* workspace.dogfather-universe.com creator Bild=404
|
||
*
|
||
* Der Bildweg (workspace-dateien.js) kannte bisher zwei Regeln: die
|
||
* Treff-Regel ("wer das Brett sieht, sieht das Bild" -- aber nur fuer
|
||
* die sieben Community-Bretter) und sonst die ABLAGE-Regel, die an
|
||
* Creator-Zuordnungen haengt. Ein Agentur-Event ist weder das eine
|
||
* noch das andere: Es gehoert niemandem, und `agentur` ist kein
|
||
* Treff-Brett. Also fiel es durch beide.
|
||
*
|
||
* Vor dem Umbau fiel das kaum auf -- das Bild war ein Anhang unter
|
||
* dem Text. Seit es die Buehne hinter dem Titel ist, saehe eine
|
||
* Creatorin eine leere Flaeche an genau der Karte, die fuer sie
|
||
* gemacht ist.
|
||
*
|
||
* WARUM EINE FUNKTION UND KEINE ZWEITE LISTE: Der naheliegende Weg
|
||
* waere gewesen, in workspace-dateien.js `|| e.bereich === "agentur"`
|
||
* zu ergaenzen. Das waere eine zweite Stelle, an der steht, wer was
|
||
* sehen darf -- und die laeuft beim naechsten Brett aus. Hier wird
|
||
* stattdessen dieselbe Regel gefragt, die auch das Brett selbst
|
||
* benutzt (sichtbarEintrag). Was dort verborgen ist, bleibt es auch
|
||
* hier; es gibt keine Rechteausweitung, nur keine Luecke mehr. */
|
||
export function darfEintragSehen(person, eintragId) {
|
||
if (!person || !Number.isInteger(Number(eintragId))) return false;
|
||
const regel = sichtbarEintrag(person);
|
||
if (!regel) return false;
|
||
return !!db().prepare(`SELECT e.id ${VERBUND} WHERE ${regel.wo} AND e.id = ?`)
|
||
.get(...regel.werte, Number(eintragId));
|
||
}
|
||
|
||
function holen(req, id) {
|
||
const regel = sichtbarEintrag(req.person);
|
||
if (!regel) return null;
|
||
return db().prepare(`SELECT e.* ${VERBUND} WHERE ${regel.wo} AND e.id = ?`)
|
||
.get(...regel.werte, id);
|
||
}
|
||
|
||
bereicheRouter.patch("/workspace/api/bereich/:bereich/:id", gleicheHerkunft, (req, res) => {
|
||
try {
|
||
const bereich = String(req.params.bereich);
|
||
const id = Number(req.params.id);
|
||
if (!BEREICHE[bereich] || !Number.isInteger(id)) return res.status(400).json({ fehler: "ungueltig" });
|
||
|
||
const eintrag = holen(req, id);
|
||
if (!eintrag || eintrag.bereich !== bereich) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
|
||
const { aus, fehler } = pruefe(bereich, req.body || {}, { neu: false });
|
||
if (fehler.length) return res.status(400).json({ fehler: fehler.join(" ") });
|
||
if (!istLeitung(req.person)) delete aus.creator_id;
|
||
|
||
/* Zeitraum beim AENDERN -- genauso wichtig wie beim Anlegen und
|
||
leichter zu uebersehen: pruefe() sieht nur die Felder, die im
|
||
Aufruf stehen. Wer nur das Ende verschiebt, schickt kein `datum`
|
||
mit; die Pruefung dort haette nichts zu vergleichen und liesse
|
||
jedes Ende durch. Deshalb hier gegen den GESPEICHERTEN Wert, und
|
||
zwar in beide Richtungen -- man kann den Zeitraum auch dadurch
|
||
verdrehen, dass man den Beginn nach hinten schiebt. */
|
||
if (bereich === "agentur" && (aus.event_ende !== undefined || aus.datum !== undefined)) {
|
||
const von = aus.datum !== undefined ? aus.datum : eintrag.datum;
|
||
const bis = aus.event_ende !== undefined ? aus.event_ende : eintrag.event_ende;
|
||
if (von && bis && bis < von) {
|
||
return res.status(400).json({ fehler: "Das Ende liegt vor dem Beginn." });
|
||
}
|
||
}
|
||
|
||
/* Beim Aendern gilt dieselbe Regel -- sonst waere die Pruefung beim
|
||
Anlegen wertlos: Man legt ohne Saeule an und haengt sie danach an. */
|
||
if (aus.saeule_id !== undefined) {
|
||
const zuWem = aus.creator_id !== undefined ? aus.creator_id : eintrag.creator_id;
|
||
const saeuleFehler = saeulePasst(aus.saeule_id, zuWem);
|
||
if (saeuleFehler) return res.status(400).json({ fehler: saeuleFehler });
|
||
}
|
||
|
||
const felder = Object.keys(aus);
|
||
if (!felder.length) return res.status(400).json({ fehler: "nichts_zu_aendern" });
|
||
|
||
db().prepare(
|
||
`UPDATE eintraege SET ${felder.map((f) => `${f} = ?`).join(", ")}, geaendert = ? WHERE id = ?`
|
||
).run(...felder.map((f) => aus[f]), jetzt(), id);
|
||
|
||
protokolliere("eintrag_geaendert", {
|
||
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
||
detail: `${bereich} #${id} ${felder.join(",")}`.slice(0, 120),
|
||
});
|
||
res.json({ ok: true });
|
||
} catch (fehler) {
|
||
console.error("[workspace] Eintrag ändern:", fehler?.message);
|
||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|
||
|
||
/* =====================================================================
|
||
EIN BILD AN EINEN BEITRAG (24.09.2026)
|
||
|
||
Filipe: "mach das man da bitte screenshots oder kurzschnitte von
|
||
den live reinposten kann. nach dem selben prinzip wie bei den
|
||
anderen nebendran."
|
||
|
||
"NACH DEM SELBEN PRINZIP" IST WOERTLICH ZU NEHMEN. Die Anzeige gab
|
||
es schon: Die Karte zeichnet ihren Bildstreifen aus "e.bilder",
|
||
die Auslieferung entscheidet die Sichtbarkeit am BEITRAG (wer das
|
||
Brett sehen darf, sieht das Bild), und die Spalte
|
||
"dateien.eintrag_id" wird seit dem Video-Einlesen benutzt. Gefehlt
|
||
hat genau ein Weg -- dieser.
|
||
|
||
WER DARF: Wer den Beitrag ueberhaupt sehen darf UND auf diesem
|
||
Brett schreiben darf. Beides zusammen, nicht eines davon: "sehen"
|
||
allein hiesse, dass die Community Bilder an fremde Beitraege
|
||
haengt; "schreiben" allein waere ein Weg, die Existenz eines
|
||
Beitrags zu erfahren, den man nicht sehen soll.
|
||
|
||
DER TYP KOMMT AUS DEM INHALT. dateiErkennen() liest die ersten
|
||
Bytes; Dateiname und Content-Type kommen vom Absender und sind
|
||
frei erfunden. Dieselbe Funktion wie im Chat und im Support --
|
||
eine zweite Fassung waere die, die beim naechsten Format vergessen
|
||
wird.
|
||
|
||
DREI BILDER JE BEITRAG. Nicht als Schikane: Wer fuenf
|
||
Bildschirmfotos anhaengt, erklaert damit nichts besser -- er
|
||
verschiebt die Arbeit zu dem, der sie ansehen muss. Und der
|
||
Bildstreifen einer Karte traegt drei nebeneinander, ohne dass die
|
||
Karte auseinanderfaellt.
|
||
===================================================================== */
|
||
const BILD_MAX_EINTRAG = 12 * 1024 * 1024;
|
||
const BILDER_JE_EINTRAG = 3;
|
||
|
||
/** Vor express.raw: Darf diese Person hier ueberhaupt etwas anhaengen?
|
||
*
|
||
* Die Berechtigung steht VOR der Annahme des Rumpfes -- sonst
|
||
* wanderten zwoelf Megabyte durch die Leitung, nur um danach
|
||
* verworfen zu werden, und jeder Angemeldete koennte an jeder
|
||
* Beitragsnummer Speicher verbrauchen. Dieselbe Reihenfolge wie beim
|
||
* Chat-Anhang. */
|
||
function darfBildAnhaengen(req, res, next) {
|
||
const bereich = String(req.params.bereich);
|
||
const id = Number(req.params.id);
|
||
if (!BEREICHE[bereich] || !Number.isInteger(id)) {
|
||
return res.status(400).json({ fehler: "ungueltig" });
|
||
}
|
||
const eintrag = holen(req, id);
|
||
if (!eintrag || eintrag.bereich !== bereich) {
|
||
return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
}
|
||
const warum = darfSchreiben(req.person, bereich);
|
||
if (warum) return res.status(403).json({ fehler: warum });
|
||
req.eintragFuerBild = eintrag;
|
||
next();
|
||
}
|
||
|
||
|
||
/* =====================================================================
|
||
EINEN PUNKT ABHAKEN (03.10.2026)
|
||
|
||
Filipe: „ich will das viel geiler viel perfekter. und auch wenn man
|
||
die eintraegt und so soll viel mehr perfektioniert und
|
||
uebersichtlicher sein."
|
||
|
||
Ein Agentur-Event stellt Bedingungen -- „jeden Tag live",
|
||
„22 Live-Stunden", „33k Diamanten". Wer mitmacht, will sehen, wo er
|
||
steht, ohne nachzufragen. Genau das ist der Unterschied zwischen
|
||
einem Aushang und einer Aktion, an der jemand teilnimmt.
|
||
|
||
JEDER HAKT NUR FUER SICH SELBST. Es gibt bewusst keinen Weg, einen
|
||
Haken bei einer ANDEREN Person zu setzen -- auch nicht fuer die
|
||
Leitung. „Ich habe meine 22 Stunden" ist eine Aussage ueber sich,
|
||
und wer sie fuer andere treffen koennte, macht aus einer Selbstaus-
|
||
kunft eine Bewertung. Die Person steht deshalb nirgends in der
|
||
Adresse; sie kommt aus der Sitzung.
|
||
|
||
KEIN KOERPER BEIM DELETE (gemessen 11.09.2026, steht so schon in
|
||
bereich.js): Nodes HTTP-Parser weist ein DELETE mit Koerper mit
|
||
einem leeren 400 ab, bevor Express die Anfrage sieht.
|
||
===================================================================== */
|
||
function punktZugang(req, res, next) {
|
||
const id = Number(req.params.id);
|
||
const schluessel = String(req.params.punkt || "");
|
||
/* Der Schluessel ist 16 Hexzeichen -- was anders aussieht, kommt
|
||
nicht aus unserer Liste und hat in der Tabelle nichts zu suchen.
|
||
Ohne diese Pruefung koennte jemand beliebigen Text ablegen und die
|
||
Tabelle als Notizbuch missbrauchen. */
|
||
if (!Number.isInteger(id) || !/^[0-9a-f]{16}$/.test(schluessel)) {
|
||
return res.status(400).json({ fehler: "ungueltig" });
|
||
}
|
||
const eintrag = holen(req, id);
|
||
if (!eintrag) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
|
||
/* GEHOERT DER PUNKT UEBERHAUPT ZU DIESEM EVENT?
|
||
|
||
Das ist der eigentliche Grund fuer diesen Waechter. Ohne ihn
|
||
koennte man einen beliebigen gueltig aussehenden Schluessel
|
||
setzen; die Zahl „3 von 3" waere danach erreichbar, ohne dass eine
|
||
der drei Aufgaben je dastand. Eine Zaehlung, die man sich selbst
|
||
ausdenken kann, zaehlt nichts.
|
||
|
||
Gefragt wird die LISTE, nicht der Text: punkteAusText ist die eine
|
||
Stelle, die entscheidet, was ein Punkt ist. */
|
||
const punkte = punkteAusText(eintrag.event_aufgaben);
|
||
if (!punkte.some((p) => p.schluessel === schluessel)) {
|
||
return res.status(404).json({ fehler: "punkt_unbekannt" });
|
||
}
|
||
req.eventEintrag = eintrag;
|
||
req.eventPunkt = schluessel;
|
||
next();
|
||
}
|
||
|
||
bereicheRouter.put("/workspace/api/eintrag/:id(\\d+)/punkt/:punkt",
|
||
angemeldet, gleicheHerkunft, punktZugang, (req, res) => {
|
||
try {
|
||
/* ON CONFLICT DO NOTHING und kein Fehler beim zweiten Mal: Zwei
|
||
Klicks kurz hintereinander (oder ein zweites Geraet) sind kein
|
||
Versehen, das man melden muesste -- das Ergebnis ist in beiden
|
||
Faellen „der Haken ist gesetzt". */
|
||
db().prepare(
|
||
"INSERT INTO event_punkte (eintrag_id, person_id, punkt, gesetzt)"
|
||
+ " VALUES (?,?,?,?) ON CONFLICT DO NOTHING")
|
||
.run(req.eventEintrag.id, req.person.id, req.eventPunkt, jetzt());
|
||
res.json(punktStand(req.eventEintrag, req.person.id));
|
||
} catch (fehler) {
|
||
console.error("[bereich] Punkt setzen:", fehler?.message);
|
||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|
||
|
||
bereicheRouter.delete("/workspace/api/eintrag/:id(\\d+)/punkt/:punkt",
|
||
angemeldet, gleicheHerkunft, punktZugang, (req, res) => {
|
||
try {
|
||
db().prepare(
|
||
"DELETE FROM event_punkte WHERE eintrag_id = ? AND person_id = ? AND punkt = ?")
|
||
.run(req.eventEintrag.id, req.person.id, req.eventPunkt);
|
||
res.json(punktStand(req.eventEintrag, req.person.id));
|
||
} catch (fehler) {
|
||
console.error("[bereich] Punkt loeschen:", fehler?.message);
|
||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|
||
|
||
/** Was nach einem Haken zurueckgeht: der gueltige Stand, vom Server.
|
||
*
|
||
* NICHT „ok". Der Browser rechnet sonst selbst weiter („war 2, jetzt
|
||
* 3") -- und liegt daneben, sobald zwei Fenster offen sind oder eine
|
||
* Aufgabe inzwischen umgeschrieben wurde. Die Zahl, die auf der Karte
|
||
* steht, kommt immer von hier. */
|
||
function punktStand(eintrag, personId) {
|
||
const punkte = punkteAusText(eintrag.event_aufgaben);
|
||
const meine = new Set(db().prepare(
|
||
"SELECT punkt FROM event_punkte WHERE eintrag_id = ? AND person_id = ?")
|
||
.all(eintrag.id, personId).map((z) => z.punkt));
|
||
return {
|
||
gesamt: punkte.length,
|
||
/* Nur zaehlen, was AUCH in der Liste steht: Wird eine Aufgabe
|
||
umgeschrieben, bleibt ihr alter Haken als Zeile liegen (das ist
|
||
richtig, siehe Tabelle) -- mitzaehlen darf er nicht. */
|
||
geschafft: punkte.filter((p) => meine.has(p.schluessel)).length,
|
||
punkte: punkte.map((p) => ({ ...p, meiner: meine.has(p.schluessel) })),
|
||
};
|
||
}
|
||
|
||
bereicheRouter.post("/workspace/api/bereich/:bereich/:id(\\d+)/bild",
|
||
gleicheHerkunft,
|
||
darfBildAnhaengen,
|
||
express.raw({ type: "*/*", limit: BILD_MAX_EINTRAG }),
|
||
(req, res) => {
|
||
let geschrieben = null;
|
||
try {
|
||
const eintrag = req.eintragFuerBild;
|
||
if (!Buffer.isBuffer(req.body) || !req.body.length) {
|
||
return res.status(400).json({ fehler: "Keine Datei empfangen." });
|
||
}
|
||
const erkannt = dateiErkennen(req.body);
|
||
if (!erkannt || erkannt.art !== "bild") {
|
||
return res.status(415).json({
|
||
fehler: "Hier geht ein Foto - PNG, JPEG, WebP oder GIF.",
|
||
});
|
||
}
|
||
|
||
const schon = db().prepare(
|
||
"SELECT COUNT(*) AS n FROM dateien WHERE eintrag_id = ?").get(eintrag.id).n;
|
||
if (schon >= BILDER_JE_EINTRAG) {
|
||
return res.status(409).json({
|
||
fehler: "An diesem Beitrag haengen schon "
|
||
+ BILDER_JE_EINTRAG + " Bilder. Nimm erst eines weg.",
|
||
});
|
||
}
|
||
|
||
mkdirSync(BILD_ORDNER_EINTRAG, { recursive: true });
|
||
const dateiname = Date.now().toString(36) + "-"
|
||
+ randomBytes(8).toString("hex") + erkannt.endung;
|
||
writeFileSync(join(BILD_ORDNER_EINTRAG, dateiname), req.body, { flag: "wx" });
|
||
geschrieben = join(BILD_ORDNER_EINTRAG, dateiname);
|
||
|
||
let roh = "";
|
||
try { roh = decodeURIComponent(req.get("x-name") || ""); } catch { roh = ""; }
|
||
const name = (roh.replace(/[\u0000-\u001f\u007f]/g, "").trim()
|
||
|| ("bild" + erkannt.endung)).slice(0, 120);
|
||
|
||
const info = db().prepare(
|
||
"INSERT INTO dateien (name_original, name_datei, groesse, typ, status,"
|
||
+ " notiz, hochgeladen_von, erstellt, eintrag_id, haus)"
|
||
+ " VALUES (?,?,?,?,'entwurf',NULL,?,?,?,?)")
|
||
.run(name, dateiname, req.body.length, erkannt.typ,
|
||
req.person.id, new Date().toISOString(), eintrag.id,
|
||
req.person.haus || null);
|
||
|
||
protokolliere("eintrag_bild", {
|
||
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
||
/* `bereichVon` hatte ich erfunden -- es gibt sie nicht. Der
|
||
Bereich steht am Eintrag selbst, und dort ist er auch
|
||
richtiger: Er kommt aus der Datenbank, nicht aus der
|
||
Adresse. */
|
||
detail: (eintrag.bereich + " #" + eintrag.id + " " + name).slice(0, 120),
|
||
});
|
||
|
||
res.status(201).json({
|
||
id: Number(info.lastInsertRowid),
|
||
name,
|
||
weg: "/workspace/api/dateien/" + Number(info.lastInsertRowid) + "/bild",
|
||
});
|
||
} catch (fehler) {
|
||
/* DIE DATEI GEHT MIT, WENN DIE ZEILE NICHT ZUSTANDE KAM. Sonst
|
||
sammelt der Ordner Bilder an, auf die nichts zeigt. */
|
||
if (geschrieben) { try { unlinkSync(geschrieben); } catch { /* egal */ } }
|
||
console.error("[bereich] Bild anhaengen:", fehler?.message);
|
||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|
||
|
||
/** Ein Bild wieder abnehmen.
|
||
*
|
||
* WER: wer den Beitrag selbst geschrieben hat -- und wer ihn ohnehin
|
||
* loeschen duerfte. Ein Bild, das nicht passt, muss jemand
|
||
* abnehmen koennen, ohne den ganzen Beitrag zu entfernen. */
|
||
bereicheRouter.delete("/workspace/api/bereich/:bereich/:id(\\d+)/bild/:bild(\\d+)",
|
||
gleicheHerkunft, (req, res) => {
|
||
try {
|
||
const bereich = String(req.params.bereich);
|
||
const id = Number(req.params.id);
|
||
const eintrag = holen(req, id);
|
||
if (!eintrag || eintrag.bereich !== bereich) {
|
||
return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
}
|
||
const treffBrett = TREFF_BRETTER.includes(bereich);
|
||
const darf = treffBrett
|
||
? TREFF_TEAM_ROLLEN.has(req.person.rolle) || eintrag.erstellt_von === req.person.id
|
||
: istLeitung(req.person) || eintrag.erstellt_von === req.person.id;
|
||
if (!darf) return res.status(403).json({ fehler: "nicht_erlaubt" });
|
||
|
||
const bild = db().prepare(
|
||
"SELECT id, name_datei FROM dateien WHERE id = ? AND eintrag_id = ?")
|
||
.get(Number(req.params.bild), eintrag.id);
|
||
if (!bild) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
|
||
db().prepare("DELETE FROM dateien WHERE id = ?").run(bild.id);
|
||
try { unlinkSync(join(BILD_ORDNER_EINTRAG, bild.name_datei)); } catch { /* schon weg */ }
|
||
/* Auch im Ablage-Ordner nachsehen: Coverbilder aus dem
|
||
Video-Einlesen liegen dort, nicht im neuen Ordner. */
|
||
try { unlinkSync(join(ABLAGE_ORDNER_EINTRAG, bild.name_datei)); } catch { /* egal */ }
|
||
|
||
protokolliere("eintrag_bild_weg", {
|
||
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
||
detail: (bereich + " #" + eintrag.id + " Bild " + bild.id).slice(0, 120),
|
||
});
|
||
res.json({ ok: true });
|
||
} catch (fehler) {
|
||
console.error("[bereich] Bild abnehmen:", fehler?.message);
|
||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|
||
|
||
bereicheRouter.delete("/workspace/api/bereich/:bereich/:id", gleicheHerkunft, (req, res) => {
|
||
try {
|
||
const bereich = String(req.params.bereich);
|
||
const id = Number(req.params.id);
|
||
if (!BEREICHE[bereich] || !Number.isInteger(id)) return res.status(400).json({ fehler: "ungueltig" });
|
||
|
||
const eintrag = holen(req, id);
|
||
if (!eintrag || eintrag.bereich !== bereich) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||
|
||
/* Löschen darf das Management und wer den Eintrag selbst geschrieben
|
||
hat -- sonst könnte ein Creator eine Notiz des Managements über
|
||
seinen eigenen Bereich verschwinden lassen. */
|
||
/* IM TREFF RAEUMT AUCH EIN MODI AUF (11.09.2026). Er ist dort die
|
||
Moderation; waere Aufraeumen der Leitung vorbehalten, bliebe ein
|
||
Beitrag stehen, bis jemand Zeit hat -- und genau in dieser Zeit
|
||
liest ihn die Community. */
|
||
const treffBrett = TREFF_BRETTER.includes(bereich);
|
||
const darfWeg = treffBrett
|
||
? TREFF_TEAM_ROLLEN.has(req.person.rolle) || eintrag.erstellt_von === req.person.id
|
||
: istLeitung(req.person) || eintrag.erstellt_von === req.person.id;
|
||
if (!darfWeg) return res.status(403).json({ fehler: "nicht_erlaubt" });
|
||
|
||
/* DER GRUND WIRD FESTGEHALTEN, BEVOR DER BEITRAG VERSCHWINDET
|
||
(DSA Art. 17). Danach waere es zu spaet: Titel und Verfasser
|
||
stuenden nirgends mehr, und eine Begruendung ohne den Beitrag,
|
||
den sie begruendet, ist keine.
|
||
|
||
DER GRUND KOMMT AUS DER ADRESSE, und das ist keine Vorliebe,
|
||
sondern gemessen (11.09.2026): Nodes HTTP-Parser weist ein
|
||
DELETE MIT KOERPER mit einem leeren 400 ab, bevor Express die
|
||
Anfrage ueberhaupt sieht -- eine Antwort mit genau einem
|
||
Kopffeld ("connection: close") und ohne Text. Wer den Fehler
|
||
sucht, sucht ihn in dieser Route, und hier ist keiner.
|
||
|
||
Der Koerper bleibt als zweite Moeglichkeit stehen: Er schadet
|
||
nicht, und wenn ein anderer Weg (ein Werkzeug, eine kuenftige
|
||
Fassung von Node) doch einen mitschickt, wird er gelesen. Zwei
|
||
Quellen fuer denselben Wert sind hier keine zwei Wahrheiten --
|
||
es ist derselbe Wert, nur zwei Briefkaesten. */
|
||
const nein = entfernenVorbereiten(req.person, eintrag,
|
||
req.body?.grund ?? req.query?.grund);
|
||
if (nein) return res.status(400).json({ fehler: nein });
|
||
|
||
db().prepare("DELETE FROM eintraege WHERE id = ?").run(id);
|
||
protokolliere("eintrag_geloescht", {
|
||
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
||
detail: `${bereich} #${id} ${eintrag.titel}`.slice(0, 120),
|
||
});
|
||
res.json({ ok: true });
|
||
} catch (fehler) {
|
||
console.error("[workspace] Eintrag löschen:", fehler?.message);
|
||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||
}
|
||
});
|