Chat: Gruender duerfen ihre Gruppe oder ihren Kanal aufloesen — und der Umschalter erklaert sich
Zwei Wuensche von Filipe, beide am Neu-Fenster. 1) "ich will eine bessere erklaerung dafuer bitte." Neben dem Umschalter "Gespraech | Kanal" stand ein Satz ueber das ANTIPPEN von Personen -- also ueber den naechsten Schritt, nicht ueber die Wahl, die gerade ansteht. Wer die beiden Woerter zum ersten Mal sieht, erfuhr nirgends, was sie bedeuten. Der Unterschied ist nicht "wenige/viele Leute", sondern WONACH der Raum benannt ist: ein Gespraech nach den Menschen darin, ein Kanal nach einem Thema. Daran haengt alles Weitere -- dass es einen Kanal je Zustaendigkeit nur einmal gibt (eindeutiger Index, nachgesehen), dass sein Name festliegt, dass die Teamleitung immer dabei ist (kanaeleAngleichen, nachgesehen) und dass Leute wechseln koennen, ohne dass der Raum ein anderer wird. Genau das steht jetzt da, und nichts davon ist behauptet. 2) "die person die ihn oeffnet soll auch das recht haben das zu loeschen und so dass es dan fuer jeden geloescht ist. aber nur die person die es gruendet." Ein EIGENER Weg (/ganz), kein Zusatzfeld am bestehenden. Es gibt jetzt zwei Loeschknoepfe nebeneinander, und sie tun etwas sehr Verschiedenes: Wegraeumen ist nur bei mir, Aufloesen ist fuer alle und endgueltig. Ein vergessenes Feld waere genau dieser Unterschied gewesen. Aus demselben Grund ein anderes Zeichen und eine eigene Warnfarbe -- zwei gleich aussehende Papierkoerbe waeren eine Falle. Nur der Gruender, woertlich: nicht die Teamleitung, nicht DogFather, nicht wer `leitung` in der Gruppe hat. Nicht bei Zweier-Gespraechen -- dort gibt es keinen Gruender, und "niemand nimmt einem anderen die Unterhaltung weg" gilt weiter. Reihenfolge beim Loeschen ist nicht beliebig: erst das Live-Ereignis (chatEreignis liest die Teilnehmer aus der Tabelle -- danach waere die Liste leer), dann die Zeilen in EINER Transaktion, dann die Anhaenge von der Platte. Umgekehrt haetten wir bei einem Ruecklauf Nachrichten, die auf geloeschte Dateien zeigen. WAS DIE PRUEFUNG GEFUNDEN HAT, BEVOR ES JEMAND GEMERKT HAETTE: Der Knopf blieb unsichtbar, obwohl das Recht stimmte. Die Oberflaeche holt den offenen Raum aus dem Nachrichten-Weg, nicht aus der Raumliste -- zwei Wege, ein Raumobjekt, und nur einer kannte das neue Feld. Die Regel steht jetzt in darfAufloesen() und wird von allen dreien benutzt: Liste, Nachrichten-Weg und der Loeschweg selbst. Gefunden hat das die Pruefung, weil sie den KNOPF misst und nicht das Recht dahinter. Haette sie nur `darf_aufloesen` geprueft, waere sie gruen gewesen und der Knopf nie erschienen. NEU: server/pruef-chat-aufloesen.mjs (59 Pruefungen). Sie misst am BESTAND, nicht an der Antwort: ob der Raum wirklich aus der Datenbank weg ist, ob keine Teilnehmerzeile liegen blieb, ob der Anhang von der Platte verschwand -- und mit Gegenprobe, dass der Anhang-Ordner selbst stehen bleibt. Ohne die waere "Datei ist weg" auch dann gruen, wenn es sie nie gab; genau das ist beim ersten Lauf passiert (der Upload lief ins 415, weil ich ihn als Formular statt roh geschickt hatte). Dazu: Wegraeumen ist NICHT Aufloesen (Ben raeumt weg, Cem hat alles noch), ein Zweier-Gespraech laesst sich gar nicht aufloesen, ein Aussenstehender bekommt 404 statt 403, ein zweiter Versuch findet nichts, und die Zustaendigkeit eines aufgeloesten Kanals wird wieder frei (sonst haette der eindeutige Index sie dauerhaft blockiert). Nebenbei: Die drei Kopfknoepfe schoben sich jeder einzeln mit `margin-left: auto` nach rechts. Bei zwei sichtbaren teilen sich zwei auto-Raender den freien Platz und reissen sie auseinander -- und WELCHE sichtbar sind, entscheidet der Server. Jetzt schiebt ein Behaelter einmal, die Knoepfe stehen beieinander, egal wie viele es sind. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
+145
-2
@@ -457,7 +457,7 @@ chatRouter.get("/workspace/api/chat/raeume", (req, res) => {
|
||||
kanaeleAngleichen(req.person);
|
||||
|
||||
const reihen = db().prepare(`
|
||||
SELECT r.id, r.art, r.name, r.kategorie, r.letzte_am,
|
||||
SELECT r.id, r.art, r.name, r.kategorie, r.letzte_am, r.erstellt_von,
|
||||
t.gelesen_bis, t.leitung, t.geloescht_bis
|
||||
FROM chat_raeume r
|
||||
JOIN chat_teilnehmer t ON t.raum_id = r.id AND t.person_id = ?
|
||||
@@ -491,6 +491,13 @@ chatRouter.get("/workspace/api/chat/raeume", (req, res) => {
|
||||
kategorie: r.kategorie || null,
|
||||
name: raumName(r, leute, req.person.id),
|
||||
leitung: !!r.leitung,
|
||||
/* DARF ICH DIESEN RAUM FUER ALLE AUFLOESEN? (11.09.2026)
|
||||
Vom Server beantwortet, nicht im Browser nachgerechnet -- die
|
||||
Oberflaeche hat `erstellt_von` sonst gar nicht. Die Regel
|
||||
selbst steht in darfAufloesen(), damit sie nicht an zwei
|
||||
Stellen steht; genau daran ist es beim ersten Anlauf
|
||||
gescheitert. */
|
||||
darf_aufloesen: darfAufloesen(r, req.person),
|
||||
teilnehmer: leute.map((t) => ({ id: t.id, name: t.name, rolle: t.rolle })),
|
||||
letzte: letzte ? {
|
||||
/* Ein Anhang ohne Begleittext hätte hier eine leere Zeile
|
||||
@@ -815,7 +822,7 @@ chatRouter.get("/workspace/api/chat/raeume/:id/nachrichten", (req, res) => {
|
||||
.get(raumId, req.person.id)?.gelesen_bis ?? 0;
|
||||
|
||||
const raum = db().prepare(
|
||||
"SELECT id, art, name, kategorie FROM chat_raeume WHERE id = ?").get(raumId);
|
||||
"SELECT id, art, name, kategorie, erstellt_von FROM chat_raeume WHERE id = ?").get(raumId);
|
||||
const leute = teilnehmerVon(raumId);
|
||||
|
||||
/* DIE ANGEHEFTETEN, in einer eigenen Abfrage.
|
||||
@@ -846,6 +853,11 @@ chatRouter.get("/workspace/api/chat/raeume/:id/nachrichten", (req, res) => {
|
||||
name: raumName(raum, leute, req.person.id),
|
||||
teilnehmer: leute.map((t) => ({ id: t.id, name: t.name, rolle: t.rolle,
|
||||
bild: t.bild, leitung: !!t.leitung })),
|
||||
/* Dasselbe Feld wie in der Liste, aus derselben Regel. Die
|
||||
Oberflaeche nimmt den offenen Raum von HIER -- ohne diese
|
||||
Zeile blieb der Aufloesen-Knopf unsichtbar, obwohl das Recht
|
||||
da war. */
|
||||
darf_aufloesen: darfAufloesen(raum, req.person),
|
||||
},
|
||||
gelesen_bis: gelesenBis,
|
||||
/* OB DER KNOPF UEBERHAUPT ERSCHEINT, entscheidet der Server.
|
||||
@@ -1161,6 +1173,137 @@ chatRouter.delete("/workspace/api/chat/raeume/:id", gleicheHerkunft, (req, res)
|
||||
}
|
||||
});
|
||||
|
||||
/** Darf diese Person diesen Raum fuer ALLE aufloesen?
|
||||
*
|
||||
* EINE REGEL, EINE STELLE (11.09.2026). Sie wurde zuerst in der
|
||||
* Raumliste ausgerechnet -- und der Knopf blieb trotzdem unsichtbar,
|
||||
* weil die Oberflaeche den offenen Raum aus dem Nachrichten-Weg
|
||||
* bezieht, nicht aus der Liste. Zwei Wege, ein Raumobjekt, und nur
|
||||
* einer kannte das Feld.
|
||||
*
|
||||
* Gefunden hat das nicht das Lesen, sondern eine Pruefung, die den
|
||||
* KNOPF gemessen hat statt des Rechts dahinter.
|
||||
*
|
||||
* `erstellt_von` muss im uebergebenen Raum stehen -- wer die Spalte
|
||||
* im SELECT vergisst, bekommt `undefined` und damit `false`. Das ist
|
||||
* die sichere Richtung: kein Knopf statt eines falschen.
|
||||
*/
|
||||
function darfAufloesen(raum, person) {
|
||||
return !!raum && raum.art !== "direkt"
|
||||
&& !!raum.erstellt_von && raum.erstellt_von === person.id;
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
EINE GRUPPE ODER EINEN KANAL AUFLOESEN — FUER ALLE (11.09.2026)
|
||||
|
||||
Filipe: "wenn ein gruppenchat oder kanal geoeffnet wird, die person
|
||||
die ihn oeffnet soll auch das recht haben das zu loeschen und so
|
||||
dass es dan fuer jeden geloescht ist. aber nur die person die es
|
||||
gruendet."
|
||||
|
||||
DAS IST ETWAS ANDERES ALS DER WEG DARUEBER, und die beiden duerfen
|
||||
nie verwechselt werden:
|
||||
|
||||
Wegraeumen -> nur bei mir. Die anderen behalten alles.
|
||||
Aufloesen -> fuer alle. Der Raum ist weg, endgueltig.
|
||||
|
||||
Deshalb ein eigener Weg (/ganz) und nicht ein Zusatzfeld am
|
||||
bestehenden: Ein vergessenes Feld waere hier der Unterschied
|
||||
zwischen "bei mir ausgeblendet" und "bei allen geloescht".
|
||||
|
||||
NUR DER GRUENDER, und zwar woertlich. Nicht die Teamleitung, nicht
|
||||
DogFather, nicht wer `leitung` in der Gruppe hat. `erstellt_von`
|
||||
steht seit jeher in der Tabelle und ist die einzige Angabe, die
|
||||
"wer hat das aufgemacht" beantwortet.
|
||||
|
||||
NICHT BEI ZWEIER-GESPRAECHEN. Dort gibt es keinen Gruender, nur
|
||||
zwei Beteiligte -- und "niemand kann einem anderen die Unterhaltung
|
||||
wegnehmen" ist die Regel von 09.09., die hier gilt.
|
||||
|
||||
DIE OFFENE STELLE, damit sie niemand suchen muss: Wird das Konto
|
||||
des Gruenders geloescht, wird `erstellt_von` auf NULL gesetzt
|
||||
(ON DELETE SET NULL) -- dann kann NIEMAND mehr aufloesen. Das ist
|
||||
die ehrliche Folge von "nur der Gruender"; wer das aendern will,
|
||||
muss eine zweite Person benennen, und das ist eine Entscheidung,
|
||||
keine Reparatur.
|
||||
|
||||
REIHENFOLGE, und sie ist nicht beliebig:
|
||||
1. Das Ereignis geht RAUS, solange es die Teilnehmer noch gibt --
|
||||
chatEreignis liest sie aus der Tabelle. Danach waere die Liste
|
||||
leer und niemand erfuehre, dass der Raum weg ist.
|
||||
2. Die Zeilen fallen in EINER Transaktion.
|
||||
3. Die Anhaenge auf der Platte erst NACH dem Commit. Andersherum
|
||||
haetten wir bei einem Ruecklauf Nachrichten, die auf Dateien
|
||||
zeigen, die es nicht mehr gibt.
|
||||
===================================================================== */
|
||||
chatRouter.delete("/workspace/api/chat/raeume/:id/ganz", gleicheHerkunft, (req, res) => {
|
||||
try {
|
||||
const raumId = Number(req.params.id);
|
||||
if (!Number.isInteger(raumId)) return res.status(400).json({ fehler: "ungueltig" });
|
||||
if (!istDrin(raumId, req.person)) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
|
||||
const d = db();
|
||||
const raum = d.prepare(
|
||||
"SELECT id, art, name, kategorie, erstellt_von FROM chat_raeume WHERE id = ?").get(raumId);
|
||||
if (!raum) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
if (raum.art === "direkt") return res.status(403).json({ fehler: "nur_gruppe_oder_kanal" });
|
||||
/* Dieselbe Regel wie fuer den Knopf -- nicht "auch so eine". Waere
|
||||
hier eine zweite Bedingung ausgeschrieben, koennte der Knopf
|
||||
eines Tages erscheinen, wo der Weg ablehnt, oder umgekehrt. */
|
||||
if (!darfAufloesen(raum, req.person)) {
|
||||
return res.status(403).json({ fehler: "nur_gruender" });
|
||||
}
|
||||
|
||||
const dateien = d.prepare(
|
||||
`SELECT anhang_datei FROM chat_nachrichten
|
||||
WHERE raum_id = ? AND anhang_datei IS NOT NULL`)
|
||||
.all(raumId).map((z) => z.anhang_datei);
|
||||
const wieViele = d.prepare(
|
||||
"SELECT COUNT(*) AS n FROM chat_nachrichten WHERE raum_id = ?").get(raumId).n;
|
||||
const leute = teilnehmerVon(raumId).map((t) => t.id);
|
||||
|
||||
/* Schritt 1: erst sagen, dann loeschen (siehe oben). */
|
||||
chatEreignis(raumId, { raum_id: raumId, aufgeloest: true }, [], "raum-weg");
|
||||
|
||||
/* Schritt 2: alles in einem Zug. Ein halb aufgeloester Raum --
|
||||
Nachrichten weg, Raum noch da -- waere in jeder Liste ein
|
||||
leerer Eintrag, den niemand mehr loswird. */
|
||||
d.exec("BEGIN");
|
||||
try {
|
||||
d.prepare("DELETE FROM chat_nachrichten WHERE raum_id = ?").run(raumId);
|
||||
d.prepare("DELETE FROM chat_teilnehmer WHERE raum_id = ?").run(raumId);
|
||||
d.prepare("DELETE FROM chat_raeume WHERE id = ?").run(raumId);
|
||||
d.exec("COMMIT");
|
||||
} catch (fehler) {
|
||||
try { d.exec("ROLLBACK"); } catch { /* schon zurueck */ }
|
||||
throw fehler;
|
||||
}
|
||||
|
||||
/* Schritt 3: die Anhaenge. Was hier liegen bleibt, ist Muell --
|
||||
aber kein Datenschaden, und es wird gemeldet statt verschwiegen. */
|
||||
let dateienWeg = 0;
|
||||
for (const name of dateien) {
|
||||
try { unlinkSync(join(ANHANG_ORDNER, name)); dateienWeg++; }
|
||||
catch (f) {
|
||||
if (f?.code === "ENOENT") dateienWeg++;
|
||||
else console.error("[chat] Anhang bleibt liegen:", f?.message);
|
||||
}
|
||||
}
|
||||
|
||||
protokolliere("chat_aufgeloest", {
|
||||
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
|
||||
detail: `${raum.art} #${raumId} "${raum.name || raum.kategorie || ""}": `
|
||||
+ `${wieViele} Nachrichten, ${leute.length} Beteiligte, `
|
||||
+ `${dateienWeg}/${dateien.length} Anhaenge`.slice(0, 200),
|
||||
});
|
||||
res.json({ ok: true, nachrichten: wieViele, beteiligte: leute.length,
|
||||
anhaenge: dateien.length });
|
||||
} catch (fehler) {
|
||||
console.error("[chat] Aufloesen:", fehler?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
/* =====================================================================
|
||||
SUCHE (09.09.2026, Punkt 15)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user