Chat zwischen Creator, Scouts und Managern
Wunsch Filipe, 06.09.2026: *"eine kategorie chat, wo die creator mit
ihren manager nachrichten austauschen koennen, wie erinnerungen, fragen
und noch vieles mehr."* Auf Nachfrage entschieden: Zweier-Gespraeche UND
Gruppen; Manager und Scouts duerfen sich auch ohne gemeinsamen Creator
schreiben.
WER MIT WEM steht an EINER Stelle: schreibbareIds() in workspace.js.
Bewusst getrennt von einladbareIds() (Terminwahl): Dort geht es darum,
wen man zu einem Kalendereintrag dazustellen darf -- harmlos. Ein Chat
legt ein dauerhaftes Gespraech an, schickt eine Benachrichtigung und
macht den eigenen Namen sichtbar. Zwei Fragen, zwei Funktionen.
Ein Creator erreicht seine Betreuer und DogFather, KEINEN fremden
Creator. Eine Gruppe ist kein Schlupfloch: Jede Nummer wird einzeln
gegen dieselbe Regel geprueft.
SOFORT STATT NACHFRAGEN IM TAKT (SSE). Ein Chat, der alle fuenf
Sekunden fragt, ist fuenf Sekunden langsam und stellt bei zehn
Angemeldeten 7200 Anfragen in der Stunde fuer Nachrichten, die es
meistens nicht gibt. Kein WebSocket: Hier fliesst alles in eine
Richtung, und SSE uebersteht einen Verbindungsabriss von selbst.
DIE ZAHL STEHT AUF JEDER SEITE, nicht nur im Chat. Eine Nachricht, die
man erst sieht, wenn man den Chat aufmacht, ist keine Nachricht --
sie ist ein Fundstueck.
Push aufs Handy nur an die, die NICHT gerade zusehen. Wer die Seite
offen hat, sieht es ohnehin; ihm zusaetzlich etwas aufs Handy zu
schicken ist der schnellste Weg, dass er Benachrichtigungen abschaltet.
WAS BEWUSST NICHT GEHT:
* Niemand liest fremde Gespraeche mit -- auch DogFather nicht. Ein
Chat, in dem der Chef stillschweigend mitliest, ist kein Chat.
Er kann jederzeit jedem schreiben, aber sichtbar.
* Eine abgeschickte Nachricht laesst sich nicht aendern, nur
zuruecknehmen. Wer Absprachen nachtraeglich umschreiben kann, macht
den Verlauf wertlos. Zurueckgenommenes hinterlaesst einen Hinweis
statt eines Lochs.
DAZU: CALL-LISTE NACH ZEIT GEGLIEDERT
Wunsch mit Bild von "STEHT AN 27": *"die liste soll kategorisiert sein
und werden mit einem button zum auf und zu machen."* Jetzt Heute /
Diese Woche / Naechste Woche / Spaeter, offen nur der erste gefuellte
Block. Nach ZEIT und nicht nach Titel: Auf dem Bild waren fast alle
"Community-Talk" -- nach Titel gruppiert haette man zwei Ueberschriften
und darunter dieselbe lange Liste.
DER FUND, DER DIE FUNKTION GERETTET HAT: Ein zugeklappter Block zeigte
seinen Inhalt trotzdem -- 367 Pixel Hoehe bei open=false. <details>
versteckt seinen Inhalt naemlich nur, solange niemand dem Inhalt eine
eigene display-Angabe gibt, und .gruppe__karten traegt display:grid.
Der Knopf haette sich bewegt und sonst nichts getan. Im Bildschirmfoto
fiel es nicht auf, weil der Ausschnitt genau darueber endete.
Ausserdem gefunden und behoben:
* Ein Wettlauf beim Abschicken: War der Ereignisstrom schneller als
die Antwort auf das POST, stand die eigene Nachricht zweimal im
Verlauf.
* Die Chatseite war zu hoch -- die Kopfleistenhoehe stand als fester
Wert (62 px) im CSS, am Handy ist sie doppelt so hoch. Das
Eingabefeld stand halb unter dem Bildschirmrand. Jetzt gemessen.
* Der Platzhalter "Enter schickt ..." brach am Handy um UND stimmte
dort nicht: Ohne Tastatur schickt Enter absichtlich nicht.
* Der Dialog benutzte Klassen aus aufgaben.css, das chat.html nicht
lud -- gemeldet von pruef-css-klassen, bevor jemand einen
ungestylten Dialog zu sehen bekam.
Pruefungen: pruef-chat (44 Punkte, Rechte und Sichtbarkeit),
pruef-chat-optik (24, zwei echte Browser schreiben sich),
pruef-call-kategorien (16).
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -666,6 +666,82 @@ export function db() {
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_aufgaben_notizen ON aufgaben_notizen (aufgabe_id);
|
||||
|
||||
/* =================================================================
|
||||
CHAT (06.09.2026)
|
||||
|
||||
Wunsch Filipe: *"eine kategorie chat, wo die creator mit ihren
|
||||
manager nachrichten austauschen können, wie erinnerungen, fragen
|
||||
und noch vieles mehr, wo jeder immer mit denen schreiben kann
|
||||
wie verbindung auch läuft."* Auf Nachfrage: Zweier-Gespraeche
|
||||
UND Gruppen, und Manager/Scouts duerfen sich auch ohne
|
||||
gemeinsamen Creator schreiben.
|
||||
|
||||
DREI TABELLEN, und die Aufteilung hat einen Grund:
|
||||
|
||||
chat_raeume das Gespraech selbst
|
||||
chat_teilnehmer wer darin ist -- und bis wohin er gelesen hat
|
||||
chat_nachrichten was gesagt wurde
|
||||
|
||||
WARUM DIE TEILNEHMER EINE EIGENE TABELLE SIND und nicht zwei
|
||||
Spalten am Raum: Ein Zweier-Gespraech kaeme mit "person_a,
|
||||
person_b" aus, eine Gruppe nicht. Zwei verschiedene Bauweisen
|
||||
fuer dieselbe Sache waeren der sichere Weg dazu, dass eine
|
||||
Abfrage die eine Haelfte vergisst. So ist ein Zweier-Gespraech
|
||||
schlicht eine Gruppe mit zwei Leuten.
|
||||
|
||||
WARUM "gelesen_bis" AM TEILNEHMER haengt und nicht an der
|
||||
Nachricht: Sonst braeuchte es je Person und Nachricht eine
|
||||
Zeile -- bei drei Leuten und tausend Nachrichten dreitausend
|
||||
Eintraege, nur um "gelesen" zu wissen. Eine Nummer je Person
|
||||
und Raum genuegt: Alles darunter ist gelesen. */
|
||||
CREATE TABLE IF NOT EXISTS chat_raeume (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
art TEXT NOT NULL DEFAULT 'direkt'
|
||||
CHECK (art IN ('direkt','gruppe')),
|
||||
/* Nur Gruppen haben einen Namen. Ein Zweier-Gespraech heisst
|
||||
immer nach dem Gegenueber -- und zwar fuer jeden anders. Ein
|
||||
gespeicherter Name waere fuer eine der beiden Seiten falsch. */
|
||||
name TEXT,
|
||||
erstellt TEXT NOT NULL,
|
||||
erstellt_von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||||
/* Der Zeitpunkt der letzten Nachricht, mitgefuehrt. Ohne ihn
|
||||
braeuchte die Gespraechsliste je Raum eine Unterabfrage --
|
||||
bei zwanzig Raeumen zwanzig Abfragen fuer eine Liste. */
|
||||
letzte_am TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_chat_raeume_letzte ON chat_raeume (letzte_am DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS chat_teilnehmer (
|
||||
raum_id INTEGER NOT NULL REFERENCES chat_raeume(id) ON DELETE CASCADE,
|
||||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||||
seit TEXT NOT NULL,
|
||||
/* Bis zu welcher Nachricht diese Person gelesen hat. */
|
||||
gelesen_bis INTEGER NOT NULL DEFAULT 0,
|
||||
/* Wer eine Gruppe angelegt hat, darf Leute hinzufuegen und
|
||||
entfernen. In Zweier-Gespraechen bedeutungslos. */
|
||||
leitung INTEGER NOT NULL DEFAULT 0,
|
||||
/* Verlassene Gruppen: Die Zeile bleibt mit einem Datum stehen,
|
||||
damit der Verlauf bis dahin lesbar bleibt und niemand
|
||||
rueckwirkend aus der Geschichte verschwindet. */
|
||||
raus_am TEXT,
|
||||
PRIMARY KEY (raum_id, person_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_chat_teilnehmer_person ON chat_teilnehmer (person_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS chat_nachrichten (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
raum_id INTEGER NOT NULL REFERENCES chat_raeume(id) ON DELETE CASCADE,
|
||||
person_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||||
text TEXT NOT NULL,
|
||||
erstellt TEXT NOT NULL,
|
||||
/* Zurueckgenommen: Der Text wird geleert, die Zeile bleibt.
|
||||
Ein Loch im Verlauf wirft mehr Fragen auf als der Hinweis,
|
||||
dass hier etwas zurueckgenommen wurde. */
|
||||
weg_am TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_chat_nachrichten_raum
|
||||
ON chat_nachrichten (raum_id, id);
|
||||
|
||||
/* Creator-Profil (Onboarding aus dem Konzept). Eine Zeile je
|
||||
Creator, entsteht erst beim ersten Speichern.
|
||||
admin_notiz ist bewusst Teil dieser Tabelle, wird aber nur an
|
||||
@@ -1146,6 +1222,11 @@ const GESCHUETZT = {
|
||||
"/workspace/uebersicht.html": null,
|
||||
"/workspace/content.html": null,
|
||||
"/workspace/aufgaben.html": null,
|
||||
/* Chat (06.09.2026): jede Rolle. WEN man erreicht, entscheidet
|
||||
schreibbareIds() -- nicht der Zugang zur Seite. Ein Creator, der
|
||||
die Seite nicht öffnen dürfte, könnte auch nicht antworten, und
|
||||
genau das Antworten ist der Zweck. */
|
||||
"/workspace/chat.html": null,
|
||||
/* NUR DogFather (02.09.2026). Die Rollenauswahl verspricht das seit
|
||||
jeher ("Manager -- dieselben Rechte, ausser der
|
||||
Personenverwaltung"), die Schranke hielt sich nur nicht daran.
|
||||
@@ -1648,6 +1729,59 @@ export function einladbareIds(person) {
|
||||
return [...new Set([...sichtbar, ...betreuerIds(person), ...chefs])];
|
||||
}
|
||||
|
||||
/** MIT WEM DARF ICH SCHREIBEN? (06.09.2026, für den Chat)
|
||||
*
|
||||
* Entschieden von Filipe: entlang der Betreuung -- PLUS Manager und
|
||||
* Scouts untereinander, auch ohne gemeinsamen Creator, "für Absprachen
|
||||
* im Team".
|
||||
*
|
||||
* WARUM NICHT einladbareIds() WIEDERVERWENDEN, obwohl es fast passt:
|
||||
* Dort geht es darum, wen man zu einem TERMIN dazustellen darf. Das
|
||||
* ist harmlos -- die Person sieht einen Eintrag im Kalender. Ein Chat
|
||||
* ist etwas anderes: Er legt ein dauerhaftes Gespräch an, schickt eine
|
||||
* Benachrichtigung und macht den eigenen Namen sichtbar. Zwei Fragen,
|
||||
* zwei Funktionen -- sonst verschiebt eine spätere Änderung an der
|
||||
* einen stillschweigend die andere.
|
||||
*
|
||||
* WAS SICH DADURCH ÄNDERT gegenüber der Terminwahl: Ein Manager
|
||||
* erreicht jetzt auch Scouts, die nicht seine sind, und umgekehrt.
|
||||
* Ein Creator NICHT -- er bleibt bei seinen Betreuern und DogFather.
|
||||
* Creator untereinander bleiben getrennt; sie sollen die Namen der
|
||||
* anderen Creator weiterhin nicht sehen.
|
||||
*
|
||||
* null = alle (nur DogFather). */
|
||||
export function schreibbareIds(person) {
|
||||
if (!person) return [];
|
||||
if (istDogFather(person)) return null;
|
||||
|
||||
const basis = new Set(einladbareIds(person) || []);
|
||||
|
||||
/* Das Team untereinander. Ein Scout, der eine Frage zu einem
|
||||
fremden Creator hat, muss den zuständigen Manager erreichen können
|
||||
-- sonst läuft alles über DogFather, und das ist genau der
|
||||
Flaschenhals, den ein Chat auflösen soll. */
|
||||
if (person.rolle === "manager" || person.rolle === "scout") {
|
||||
try {
|
||||
for (const z of db().prepare(
|
||||
"SELECT id FROM personen WHERE rolle IN ('manager','scout') AND aktiv = 1").all()) {
|
||||
basis.add(z.id);
|
||||
}
|
||||
} catch { /* im Zweifel nur die Betreuungskette */ }
|
||||
}
|
||||
|
||||
/* Sich selbst nicht -- ein Gespräch mit sich allein ist keines. */
|
||||
basis.delete(person.id);
|
||||
return [...basis];
|
||||
}
|
||||
|
||||
/** Darf diese Person mit jener schreiben? Eine Frage, eine Antwort --
|
||||
* damit die Regel nicht an fünf Stellen einzeln nachgebaut wird. */
|
||||
export function darfSchreibenMit(person, andereId) {
|
||||
const ids = schreibbareIds(person);
|
||||
if (ids === null) return Number(andereId) !== Number(person.id);
|
||||
return ids.includes(Number(andereId));
|
||||
}
|
||||
|
||||
/** SQL-Baustein daraus: "diese Spalte ist eine Person, die ich sehen
|
||||
* darf". Gibt null zurück, wenn nicht eingeschränkt werden muss. */
|
||||
export function personenWo(person, spalte) {
|
||||
|
||||
Reference in New Issue
Block a user