Filipe, 22.09.2026: "ersetze die zentrale durch, Die IrrenAnstalt.
genau so geschrieben bitte. und alles was treff heißt oder wo treff
steht soll durch Rudel ersetzt werden bitte."
Die Schreibweise "IrrenAnstalt" mit grossem A in der Mitte ist so
gewollt. Das steht als Hinweis daneben, damit sie beim naechsten Mal
niemand "korrigiert".
WAS UMBENANNT WURDE: alles, was jemand LIEST -- Kachelnamen,
Ueberschriften, Markenzeilen, Saetze, Meldungen, Aufgabenvorlagen.
116 Zeilen in 42 Dateien.
WAS BLEIBT: Adressen (treff-regeln.html), Bezeichner (TREFF_ROLLEN),
Datenbankwerte (bereich = 'treff'), Abfrageteile (b=treff). Eine
Adresse umzubenennen bricht jedes Lesezeichen, und ein Datenbankwert
umzuschreiben waere eine Umstellung ohne Gegenwert. Dieselbe
Entscheidung wie heute frueh bei Dogi-Media, wo material.html auch
material.html geblieben ist.
DREI GRAMMATIKFEHLER IM EIGENEN ENTWURF, alle vor dem Ausliefern
gefunden -- ein blindes Ersetzen reicht hier nicht:
1. "der Treff" ist maennlich, "das Rudel" saechlich. Ohne Tabelle
waere ueberall "Der Rudel" gestanden. (Und "Treffer" waere zu
"Rudeler" geworden, "Treffen" zu "Rudelen" -- deshalb greift die
Regel nur bei Wortgrenze und nie vor einem Kleinbuchstaben.)
2. BINDESTRICH-ZUSAMMENSETZUNGEN. In "der Treff-Chat" gehoert der
Artikel zu "Chat", nicht zu "Treff". Der erste Durchlauf machte
daraus "das Rudel-Chat", "das Rudel-Kacheln" und "ein Rudel-Raum".
Jetzt greift die Artikelregel nur, wenn das Wort allein steht.
3. GESCHUETZTE LEERZEICHEN. In den Markenzeilen steht
`Der Treff`, damit die zwei Woerter nicht umbrechen. Das
Muster hat daran vorbeigegriffen: "Der Rudel", auf fuenf
Seiten. Jetzt wird das Trennzeichen mitgefasst und unveraendert
wieder eingesetzt.
Gefunden wurden alle drei, weil jede einzelne der 116 Zeilen vor dem
Schreiben als ALT/NEU ausgegeben und gelesen wurde -- und danach
gezielt nach falschen Artikeln gesucht ("den Rudel", "der Rudel",
"einen Rudel"). Uebrig blieben sechs Treffer, und die sind alle
richtig: "einen Rudel-Raum", "der Rudel-Chat", "den Rudel-Regeln" --
Zusammensetzungen, bei denen der Artikel zum letzten Wort gehoert.
UND ZWEI PRUEFUNGEN, die die Umbenennung nicht mitbekommen haetten:
`/Treff/.test(...)` und `/Treff/i.test(...)`. Ein regulaerer Ausdruck
ist keine Zeichenkette -- sie haetten ab sofort nach einem Wort
gesucht, das die Seite nicht mehr sagt, und waeren still rot geworden,
ohne dass etwas kaputt ist.
Geprueft, alle gruen: pruef-treff, pruef-treffchat,
pruef-deutsche-texte, pruef-uebernahme, pruef-crew-adresse,
pruef-willkommen, pruef-wege-nach-draussen, pruef-vorlagen,
pruef-start-ansicht, pruef-struktur.
Co-Authored-By: Claude Opus 5 <[email protected]>
285 lines
12 KiB
JavaScript
285 lines
12 KiB
JavaScript
/* =====================================================================
|
||
DER TREFF-CHAT — mit Nachtruhe (19.09.2026)
|
||
|
||
Filipe, auf die Frage, ob die Community einen Chat bekommen soll:
|
||
|
||
„eine geile mischung von punkt 2 und drei. mach es perfekt so das
|
||
quasi nach mitternacht bis morgens 6 uhr keiner schreiben kann
|
||
damit auch die privatsphäre berücksichtig wird über nacht.
|
||
perfektionier das bitte. mach es übelst krass geil. und die
|
||
sollen nicht anrufen können. nur schreiben."
|
||
|
||
Zur Auswahl standen „gar kein Chat", „nur mit Öffnungszeiten" und
|
||
„offen wie im Team". Seine Antwort ist die dritte Möglichkeit, die
|
||
nicht auf dem Zettel stand, und sie ist die bessere: offen den
|
||
ganzen Tag, ZU über Nacht.
|
||
|
||
---------------------------------------------------------------------
|
||
WARUM DAS MEHR IST ALS EINE EINSTELLUNG
|
||
|
||
Der Grund, den er nennt, ist nicht Bequemlichkeit, sondern
|
||
Privatsphäre — und zwar die der Leute, die moderieren. Die Forschung
|
||
zu ehrenamtlichen Moderatoren ist an diesem Punkt eindeutig: Die
|
||
beiden meistgenannten Gründe fürs Aufhören sind ZU WENIG ZEIT und
|
||
STREIT IM TEAM. Ein Chat, der nachts um drei weiterläuft, erzeugt
|
||
beides — die Nachricht steht da, jemand fühlt sich zuständig, und am
|
||
nächsten Morgen ist aus einem Satz ein Streit geworden, den niemand
|
||
früh genug gesehen hat.
|
||
|
||
Eine Nachtruhe nimmt diesen Druck heraus, ohne irgendjemandem etwas
|
||
zu verbieten: Gelesen werden darf weiter, geschrieben ab sechs.
|
||
|
||
---------------------------------------------------------------------
|
||
SIE GILT FÜR ALLE, AUCH FÜR DIE LEITUNG
|
||
|
||
Das ist die wichtigste Entscheidung dieser Datei, und sie ist
|
||
unbequem. Der bequeme Weg wäre, DogFather und die rechte Hand
|
||
auszunehmen — sie sollen ja jederzeit eingreifen können.
|
||
|
||
Aber: Wenn DogFather um halb vier schreibt, liest es jemand. Und wer
|
||
es liest, fühlt sich zuständig. Eine Nachtruhe, von der die Leitung
|
||
ausgenommen ist, ist keine Nachtruhe, sondern eine Bitte — und
|
||
Bitten dieser Art halten genau so lange, bis es einmal eilig ist.
|
||
|
||
MODERIEREN GEHT TROTZDEM. Verbergen, löschen, jemanden
|
||
herausnehmen — das sind keine Nachrichten, sondern Eingriffe, und
|
||
die bleiben rund um die Uhr möglich. Wer nachts etwas Schlimmes
|
||
sieht, kann es wegnehmen; er kann nur nicht darüber diskutieren.
|
||
|
||
UND DER VERTRAULICHE MELDEWEG BLEIBT OFFEN. Er ist kein Chat,
|
||
sondern ein Fall mit einer Zuständigkeit. Wer nachts Hilfe braucht,
|
||
hat sie dort — nicht in einem Raum mit dreißig Leuten.
|
||
|
||
---------------------------------------------------------------------
|
||
GERECHNET WIRD IN ORTSZEIT, NICHT IN UTC
|
||
|
||
`new Date().getHours()` liefert die Stunde des SERVERS. Steht der auf
|
||
UTC, geht der Chat im Sommer um zwei Uhr nachts zu und um acht Uhr
|
||
morgens wieder auf — beides zwei Stunden daneben, und beides fällt
|
||
nur denen auf, die um diese Zeit wach sind.
|
||
|
||
Dieses Haus ist mit genau dieser Rechnung schon zweimal
|
||
hereingefallen (06.09. und 16.09., beide Male in Prüfungen, beide
|
||
Male nur nachts sichtbar). Deshalb steht hier `Intl` mit
|
||
`Europe/Berlin` und nicht `getHours()`.
|
||
|
||
---------------------------------------------------------------------
|
||
KEINE ANRUFE — UND DER RIEGEL SITZT AN EINER STELLE
|
||
|
||
„die sollen nicht anrufen können. nur schreiben."
|
||
|
||
Ein Anruf hängt in diesem Haus an der RAUMMITGLIEDSCHAFT
|
||
(`istDrin`). Sobald die Community einen Raum hat, könnte sie also
|
||
telefonieren — nicht, weil es jemand erlaubt hätte, sondern weil es
|
||
niemand verboten hat. Das ist die gefährlichste Art von Erlaubnis.
|
||
|
||
Der Riegel steht deshalb GANZ VORNE am Anruf-Router, nicht in den
|
||
neun Routen dahinter. Neun Stellen hieße acht richtige und eine
|
||
vergessene, und die vergessene wäre die, die keiner ausprobiert.
|
||
===================================================================== */
|
||
|
||
import { db, darfTelefonieren } from "./workspace.js";
|
||
import { AUSSEN_ROLLEN } from "./crew-adresse.js";
|
||
|
||
/** Die Kategorie des Treff-Kanals.
|
||
*
|
||
* ALS KANAL UND NICHT ALS EIGENE RAUMART: `chat_raeume.art` hat eine
|
||
* CHECK-Bedingung ('direkt','gruppe','kanal'). Eine vierte Art hieße,
|
||
* die Tabelle an lebenden Daten neu zu bauen — für etwas, das eine
|
||
* Kategorie genauso gut sagt. Ein Kanal ist ohnehin genau das
|
||
* Richtige: Er gehört dem Haus, nicht den Anwesenden, und die
|
||
* Leitung kommt über `istDrin` hinein, ohne dass jemand sie einträgt. */
|
||
export const TREFF_KATEGORIE = "treff";
|
||
export const TREFF_NAME = "Das Rudel";
|
||
|
||
/** Wer im Treff-Chat steht.
|
||
*
|
||
* Die Community UND die, die moderieren. Ein Raum voller Zuschauer
|
||
* ohne Moderation wäre nicht mutig, sondern fahrlässig — und die
|
||
* Nachtruhe ist kein Ersatz dafür, sie verkürzt nur die Zeit, in der
|
||
* jemand hinsehen muss. */
|
||
export const TREFF_ROLLEN = ["gast", "modi", "hand", "linke", "admin"];
|
||
|
||
/** Ab wann und bis wann niemand schreibt — Ortszeit, Stunden.
|
||
*
|
||
* 0 BIS 6, wörtlich nach Filipes Ansage („nach mitternacht bis
|
||
* morgens 6 uhr"). Als Umgebungsvariable, damit eine andere Zeit
|
||
* keine Programmänderung ist — dieselbe Bauweise wie bei der
|
||
* Push-Ruhezeit (22–7, die ist eine ANDERE Sache und bleibt getrennt:
|
||
* dort geht es darum, wann das Telefon klingelt, hier darum, wann
|
||
* überhaupt jemand schreiben kann). */
|
||
export const NACHT_AB = Number(process.env.TREFF_NACHT_AB ?? 0);
|
||
export const NACHT_BIS = Number(process.env.TREFF_NACHT_BIS ?? 6);
|
||
|
||
/** Die Stunde in Europe/Berlin — nicht die des Servers. */
|
||
const STUNDE = new Intl.DateTimeFormat("de-DE", {
|
||
timeZone: "Europe/Berlin", hour: "2-digit", hour12: false,
|
||
});
|
||
export function stundeInBerlin(d = new Date()) {
|
||
/* „24" gibt es: Manche Umgebungen liefern fuer Mitternacht 24 statt
|
||
0. Ungeprueft waere der Chat dann ausgerechnet in der ersten
|
||
Stunde der Nachtruhe offen. */
|
||
const s = Number(STUNDE.format(d).replace(/\D/g, ""));
|
||
return s === 24 ? 0 : s;
|
||
}
|
||
|
||
/** Die Minute in Europe/Berlin — für den Countdown. */
|
||
const MINUTE = new Intl.DateTimeFormat("de-DE", {
|
||
timeZone: "Europe/Berlin", minute: "2-digit",
|
||
});
|
||
function minuteInBerlin(d = new Date()) {
|
||
return Number(MINUTE.format(d).replace(/\D/g, ""));
|
||
}
|
||
|
||
/** Schläft der Chat gerade?
|
||
*
|
||
* ÜBER MITTERNACHT HINWEG GEDACHT. Mit AB=0 und BIS=6 ist es der
|
||
* einfache Fall (0 ≤ s < 6). Setzt jemand später AB=23, muss die
|
||
* Rechnung den Tageswechsel mitmachen — sonst wäre die Nachtruhe
|
||
* zwischen 23 und 24 Uhr aus. Beide Fälle stehen hier, damit die
|
||
* Einstellung wirklich eine Einstellung ist und keine Falle. */
|
||
export function istNachtruhe(d = new Date()) {
|
||
if (NACHT_AB === NACHT_BIS) return false; // keine Nachtruhe
|
||
const s = stundeInBerlin(d);
|
||
return NACHT_AB < NACHT_BIS
|
||
? (s >= NACHT_AB && s < NACHT_BIS)
|
||
: (s >= NACHT_AB || s < NACHT_BIS);
|
||
}
|
||
|
||
/** Wie viele Minuten noch, bis sich der Zustand ändert.
|
||
*
|
||
* Für den Countdown auf der Seite. Er ist der Unterschied zwischen
|
||
* „der Chat ist plötzlich zu" und „ich weiß seit zehn Minuten, dass
|
||
* gleich Schluss ist". */
|
||
export function minutenBisWechsel(d = new Date()) {
|
||
const s = stundeInBerlin(d), m = minuteInBerlin(d);
|
||
const jetztMin = s * 60 + m;
|
||
const ziel = (istNachtruhe(d) ? NACHT_BIS : NACHT_AB) * 60;
|
||
let diff = ziel - jetztMin;
|
||
if (diff <= 0) diff += 24 * 60;
|
||
return diff;
|
||
}
|
||
|
||
/** Alles, was die Seite über die Nachtruhe wissen muss — an einer Stelle.
|
||
*
|
||
* Die Oberfläche rechnet NICHTS davon selbst nach. Eine zweite
|
||
* Rechnung im Browser wäre die, die bei der nächsten Zeitumstellung
|
||
* anders herauskommt als die im Server — und dann sagt die Seite
|
||
* „offen", während der Server ablehnt. */
|
||
export function nachtruheStand(d = new Date()) {
|
||
const zu = istNachtruhe(d);
|
||
return {
|
||
zu,
|
||
ab: NACHT_AB,
|
||
bis: NACHT_BIS,
|
||
minuten: minutenBisWechsel(d),
|
||
/* Der fertige Satz kommt vom Server, nicht aus dem Browser. So
|
||
steht er in der Absage UND auf der Seite gleich — und wer ihn
|
||
ändern will, ändert ihn einmal. */
|
||
satz: zu
|
||
? `Das Rudel schläft. Ab ${String(NACHT_BIS).padStart(2, "0")}:00 Uhr `
|
||
+ "geht es weiter – gelesen werden darf die ganze Nacht."
|
||
: `Nachts zwischen ${String(NACHT_AB).padStart(2, "0")}:00 und `
|
||
+ `${String(NACHT_BIS).padStart(2, "0")}:00 Uhr schreibt hier niemand.`,
|
||
};
|
||
}
|
||
|
||
/** Darf diese Person überhaupt telefonieren?
|
||
*
|
||
* EINE FRAGE, EINE ANTWORT. Sie wird ganz vorne am Anruf-Router
|
||
* gestellt, nicht in den neun Routen dahinter.
|
||
*
|
||
* GERECHNET WIRD SIE IN workspace.js, weil `/api/ich` sie ebenfalls
|
||
* braucht -- und jene Datei kann nicht aus dieser importieren, ohne
|
||
* einen Ring zu bilden. Hier steht nur die Durchreichung, damit der
|
||
* Anruf-Router und der Chat sie unter demselben Namen finden. */
|
||
export { darfTelefonieren };
|
||
|
||
/** Gehört diese Person in den Treff-Chat? */
|
||
export function gehoertInTreff(person) {
|
||
return TREFF_ROLLEN.includes(person?.rolle);
|
||
}
|
||
|
||
/* ---------------------------------------------------------------------
|
||
DER RAUM
|
||
------------------------------------------------------------------- */
|
||
|
||
const jetzt = () => new Date().toISOString();
|
||
|
||
/** Die Nummer des Treff-Kanals — angelegt, falls es ihn noch nicht gibt.
|
||
*
|
||
* GENAU EINER. Zwei Räume mit demselben Zweck sind zwei Räume mit je
|
||
* der Hälfte der Nachrichten darin, und niemand merkt, in welchem er
|
||
* gerade steht. Deshalb wird gesucht, bevor angelegt wird. */
|
||
export function treffRaumId() {
|
||
const d = db();
|
||
const da = d.prepare(
|
||
"SELECT id FROM chat_raeume WHERE art = 'kanal' AND kategorie = ?")
|
||
.get(TREFF_KATEGORIE);
|
||
if (da) return da.id;
|
||
|
||
d.prepare(`INSERT INTO chat_raeume (art, name, kategorie, erstellt, erstellt_von)
|
||
VALUES ('kanal', ?, ?, ?, NULL)`)
|
||
.run(TREFF_NAME, TREFF_KATEGORIE, jetzt());
|
||
const id = d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||
console.log(`[treff] Chat-Raum angelegt (Nummer ${id}).`);
|
||
return id;
|
||
}
|
||
|
||
/** Trägt ein, wer hineingehört und noch fehlt.
|
||
*
|
||
* SELBSTHEILEND, wie der Kanal-Abgleich der Teamführung nebenan —
|
||
* und aus demselben Grund: Die Zeile müsste sonst beim Anlegen einer
|
||
* Person, beim Rollenwechsel und beim Anlegen des Raums entstehen.
|
||
* Drei Stellen, von denen die dritte vergessen wird.
|
||
*
|
||
* `gelesen_bis` startet am aktuellen Stand. Wer heute dazukommt, hat
|
||
* die vierhundert Nachrichten von vorher nicht verpasst — eine rote
|
||
* Vierhundert wäre keine Auskunft, sondern Rauschen.
|
||
*
|
||
* WER AUSGETRETEN IST, BLEIBT DRAUSSEN. `raus_am IS NOT NULL` heißt:
|
||
* Die Moderation hat jemanden herausgenommen. Ihn beim nächsten
|
||
* Seitenaufruf still wieder einzutragen wäre die zuverlässigste Art,
|
||
* eine Moderationsentscheidung unwirksam zu machen. */
|
||
export function treffAngleichen() {
|
||
try {
|
||
const id = treffRaumId();
|
||
const d = db();
|
||
const platz = TREFF_ROLLEN.map(() => "?").join(", ");
|
||
const fehlen = d.prepare(`
|
||
SELECT p.id FROM personen p
|
||
WHERE p.aktiv = 1 AND p.rolle IN (${platz})
|
||
AND NOT EXISTS (SELECT 1 FROM chat_teilnehmer t
|
||
WHERE t.raum_id = ? AND t.person_id = p.id)`)
|
||
.all(...TREFF_ROLLEN, id);
|
||
if (!fehlen.length) return 0;
|
||
|
||
const rein = d.prepare(`
|
||
INSERT INTO chat_teilnehmer (raum_id, person_id, seit, gelesen_bis, leitung)
|
||
SELECT ?, ?, ?, COALESCE((SELECT MAX(id) FROM chat_nachrichten WHERE raum_id = ?), 0), ?`);
|
||
const n = jetzt();
|
||
for (const p of fehlen) {
|
||
/* Leitung im Raum sind die, die auch sonst moderieren. Ein Gast
|
||
ist Teilnehmer, kein Leiter -- sonst dürfte er anheften und
|
||
andere herausnehmen. */
|
||
const person = d.prepare("SELECT rolle FROM personen WHERE id = ?").get(p.id);
|
||
const leitung = person && person.rolle !== "gast" ? 1 : 0;
|
||
rein.run(id, p.id, n, id, leitung);
|
||
}
|
||
return fehlen.length;
|
||
} catch (fehler) {
|
||
/* Ein Fehler hier darf die Gespraechsliste nicht kosten. */
|
||
console.error("[treff] Abgleich:", fehler?.message);
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
/** Ist das der Treff-Raum? */
|
||
export function istTreffRaum(raumId) {
|
||
try {
|
||
const r = db().prepare(
|
||
"SELECT kategorie FROM chat_raeume WHERE id = ? AND art = 'kanal'").get(raumId);
|
||
return r?.kategorie === TREFF_KATEGORIE;
|
||
} catch { return false; }
|
||
}
|