diff --git a/server/pruef-chat-optik.mjs b/server/pruef-chat-optik.mjs
index 5baaf6a7..afbaf5fb 100644
--- a/server/pruef-chat-optik.mjs
+++ b/server/pruef-chat-optik.mjs
@@ -1065,6 +1065,157 @@ melde("\n=== Wer hat reagiert? ===");
" das Vorlesewort der Kachel nennt sie auf beiden Geräten");
}
+/* =====================================================================
+ BEARBEITEN -- AM ECHTEN BROWSER (03.10.2026)
+
+ Filipe: "jeder soll seine eigene nachricht bearbeiten koennen."
+
+ Die Route ist in pruef-chat gemessen. Hier geht es um das, was eine
+ Route nicht zeigen kann: Steht der Knopf da, wo man ihn braucht,
+ steht er NICHT an fremden Nachrichten, und kommt der neue Text bei
+ dem an, der daneben sitzt -- ohne dass er neu laedt.
+
+ DIE LETZTE FRAGE IST DIE WICHTIGSTE. Ein Bearbeiten, das nur der
+ Schreibende sieht, ist schlimmer als keins: Zwei Leute haetten
+ denselben Satz verschieden im Bild und wuessten es nicht.
+ ===================================================================== */
+console.log("\n=== Eine eigene Nachricht bearbeiten (im Browser) ===");
+{
+ /* EIGENE SITZUNGEN, NICHT DIE VON OBEN. Die Namen `luna` und `pat`
+ gibt es hier noch, ihre Kontexte sind aber seit Abschnitt 3
+ geschlossen -- und Playwright meldet dann nur "Target page has
+ been closed", nicht, woran es liegt. Genau daran bin ich hier
+ einmal haengengeblieben. */
+ const luna2 = await alsRolle("creator", "CODE-CREA-0001");
+ const pat2 = await alsRolle("scout", "CODE-SCOU-0001");
+
+ /* DERSELBE RAUM BEI BEIDEN -- und zwar ueber die NUMMER, nicht ueber
+ "den ersten in der Liste" (03.10.2026).
+
+ Der erste Anlauf liess beide auf `.chat-raum__knopf` klicken, also
+ jeweils auf ihr oberstes Gespraech. Inzwischen gibt es in diesem
+ Lauf mehrere: Luna landete in ihrem, Patrick in einem anderen, und
+ die Messung meldete "Patrick sieht die Nachricht nicht" -- ein
+ Befund, der dem Testaufbau gehoerte und nicht dem Haus.
+
+ Schlimmer war die Nebenwirkung: Die Pruefung "an ihr steht KEIN
+ bearbeiten" war gruen, weil die Nachricht ueberhaupt nicht da war.
+ Ein gruener Haken fuer ein Element, das es nicht gibt -- genau die
+ Sorte, die nichts beweist. Deshalb haengt sie unten jetzt daran,
+ dass er sie wirklich sieht. */
+ await luna2.seite.waitForSelector(".chat-raum__knopf", { timeout: 10000 });
+ const raumNr = await luna2.seite.evaluate(() => {
+ const li = [...document.querySelectorAll("li[data-id]")]
+ .find((x) => /Patrick/.test(x.textContent));
+ return li ? li.dataset.id : null;
+ });
+ ok(!!raumNr, `das Gespraech mit Patrick steht in Lunas Liste (#${raumNr})`);
+ for (const wer of [luna2, pat2]) {
+ await wer.seite.waitForSelector(`li[data-id="${raumNr}"] .chat-raum__knopf`, { timeout: 10000 });
+ await wer.seite.click(`li[data-id="${raumNr}"] .chat-raum__knopf`);
+ await wer.seite.waitForSelector("#schreiben:not([hidden])", { timeout: 10000 });
+ }
+ await luna2.seite.waitForTimeout(600);
+
+ await luna2.seite.fill("#text", "Treffen um 15 Urh");
+ await luna2.seite.click("#senden");
+ await luna2.seite.waitForTimeout(900);
+
+ /* Bei Luna steht der Knopf, bei Patrick an derselben Nachricht
+ nicht -- das ist die Gegenprobe, und sie laeuft am SELBEN Satz. */
+ const knopfBeiLuna = await luna2.seite.evaluate(() => {
+ const k = [...document.querySelectorAll(".chat-nachricht")]
+ .find((x) => /15 Urh/.test(x.textContent));
+ return !!k?.querySelector(".chat-nachricht__bearb-knopf");
+ });
+ ok(knopfBeiLuna, "an der eigenen Nachricht steht „bearbeiten“");
+
+ await pat2.seite.waitForTimeout(600);
+ const knopfBeiPat = await pat2.seite.evaluate(() => {
+ const k = [...document.querySelectorAll(".chat-nachricht")]
+ .find((x) => /15 Urh/.test(x.textContent));
+ return { da: !!k, knopf: !!k?.querySelector(".chat-nachricht__bearb-knopf") };
+ });
+ /* DIE ZWEITE ZEILE HAENGT AN DER ERSTEN. "Kein Knopf" ist sonst auch
+ wahr, wenn die Nachricht gar nicht da ist -- und dann prueft die
+ Zeile nichts. */
+ ok(knopfBeiPat.da, "Patrick sieht dieselbe Nachricht");
+ ok(knopfBeiPat.da && knopfBeiPat.knopf === false,
+ "und an ihr KEIN „bearbeiten“ — fremde Worte aendert niemand");
+
+ /* Luna oeffnet das Feld und richtet den Tippfehler. */
+ await luna2.seite.evaluate(() => {
+ [...document.querySelectorAll(".chat-nachricht")]
+ .find((x) => /15 Urh/.test(x.textContent))
+ ?.querySelector(".chat-nachricht__bearb-knopf")?.click();
+ });
+ await luna2.seite.waitForSelector(".chat-bearb__feld", { timeout: 8000 });
+ const feldGross = await luna2.seite.evaluate(() => {
+ const f = document.querySelector(".chat-bearb__feld");
+ const r = f.getBoundingClientRect();
+ return { schrift: parseFloat(getComputedStyle(f).fontSize), hoch: Math.round(r.height),
+ wert: f.value };
+ });
+ ok(feldGross.wert === "Treffen um 15 Urh",
+ `im Feld steht der alte Text ("${feldGross.wert}")`);
+ /* 16 px, sonst zoomt iOS beim Hineintippen die ganze Seite heran. */
+ ok(feldGross.schrift >= 16, `Schrift im Feld ${feldGross.schrift} px (mindestens 16)`);
+
+ await luna2.seite.fill(".chat-bearb__feld", "Treffen um 15 Uhr");
+ await luna2.seite.click(".chat-bearb__knopf--ja");
+ await luna2.seite.waitForTimeout(900);
+
+ const beiLunaNach = await luna2.seite.evaluate(() => {
+ const k = [...document.querySelectorAll(".chat-nachricht")]
+ .find((x) => /15 Uhr/.test(x.textContent));
+ return {
+ text: k?.querySelector(".chat-nachricht__text")?.textContent,
+ vermerk: k?.querySelector(".chat-nachricht__bearbeitet")?.textContent || null,
+ feldNochDa: !!document.querySelector(".chat-bearb__feld"),
+ };
+ });
+ ok(beiLunaNach.text === "Treffen um 15 Uhr",
+ `bei ihr steht der neue Text ("${beiLunaNach.text}")`);
+ ok(beiLunaNach.vermerk === "bearbeitet",
+ `und daneben der Vermerk (${beiLunaNach.vermerk})`);
+ ok(beiLunaNach.feldNochDa === false, "das Feld ist wieder zu");
+
+ /* DIE VORSCHAU LINKS MUSS MITKOMMEN (03.10.2026).
+
+ Diese Zeile gibt es, weil ein Bildschirmfoto sie erzwungen hat:
+ Rechts stand schon "Treffen um 15 Uhr", links in der
+ Gespraechsliste weiter "Du: Treffen um 15 Urh". Derselbe Satz,
+ zweimal verschieden, auf einem Schirm -- und keine einzige
+ Messung hat es gemerkt, weil beide Haelften fuer sich stimmten. */
+ await luna2.seite.waitForTimeout(700);
+ const vorschau = await luna2.seite.evaluate((nr) =>
+ document.querySelector(`li[data-id="${nr}"]`)?.textContent || "", raumNr);
+ ok(/15 Uhr/.test(vorschau) && !/15 Urh/.test(vorschau),
+ `die Vorschau in der Liste zeigt den neuen Text ("${vorschau.replace(/\s+/g, " ").trim()}")`);
+
+ /* UND BEI PATRICK -- ohne Neuladen, ueber den Ereignisstrom. */
+ await pat2.seite.waitForTimeout(1200);
+ const beiPatNach = await pat2.seite.evaluate(() => {
+ const alle = [...document.querySelectorAll(".chat-nachricht")];
+ const k = alle.find((x) => /15 Uhr/.test(x.textContent));
+ return {
+ neu: k?.querySelector(".chat-nachricht__text")?.textContent,
+ vermerk: !!k?.querySelector(".chat-nachricht__bearbeitet"),
+ altNochDa: alle.some((x) => /15 Urh/.test(x.textContent)),
+ };
+ });
+ ok(beiPatNach.neu === "Treffen um 15 Uhr",
+ `bei Patrick steht er auch -- ohne Neuladen ("${beiPatNach.neu}")`);
+ ok(beiPatNach.vermerk, "mit demselben Vermerk");
+ ok(beiPatNach.altNochDa === false,
+ "und der alte Satz ist nicht doppelt stehengeblieben");
+
+ await luna2.seite.screenshot({ path: new URL("./pruef-chat-bearbeiten.png", import.meta.url)
+ .pathname.replace(/^\/([A-Za-z]:)/, "$1") });
+ await luna2.ctx.close();
+ await pat2.ctx.close();
+}
+
await browser.close();
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
diff --git a/server/pruef-chat.mjs b/server/pruef-chat.mjs
index 89549df3..d9a271d9 100644
--- a/server/pruef-chat.mjs
+++ b/server/pruef-chat.mjs
@@ -313,6 +313,121 @@ console.log("\n=== Ungelesene Nachrichten ===");
ok(zurueck === 0, `ein alter "gelesen bis 1" dreht nichts zurück (${zurueck})`);
}
+/* =======================================================================
+ 6b. EIGENE NACHRICHT BEARBEITEN (03.10.2026)
+
+ Filipe: "die nachrichten die man in den chat reinschreibt. jeder
+ soll seine eigene nachricht bearbeiten koennen."
+
+ DIE WICHTIGSTE ZEILE IST DIE GEGENPROBE MIT DOGFATHER. Er darf
+ JEDE Nachricht loeschen -- fuer den Notfall, so steht es seit dem
+ 22.09. im Haus. Beim Bearbeiten darf er das ausdruecklich NICHT:
+ Eine fremde Nachricht zu entfernen heisst "das soll hier nicht
+ stehen"; sie zu AENDERN heisst, jemandem Worte in den Mund zu
+ legen, die unter seinem Namen und seinem Bild stehen bleiben.
+ Waere der Loeschweg hier blind mituebernommen worden, haette es
+ niemandem auffallen muessen -- es sieht ja aus wie dieselbe
+ Befugnis.
+ ======================================================================= */
+console.log("\n=== Eine eigene Nachricht bearbeiten ===");
+{
+ const a = await rufe(luna.keks, `/workspace/api/chat/raeume/${raumLunaPat}/nachrichten`,
+ { text: "Ich kome moregn" }, "POST");
+ const { nachricht } = await a.json();
+ ok(!!nachricht?.id, `Luna schreibt etwas mit Tippfehlern (#${nachricht?.id})`);
+
+ /* Vorher merken: Rueckt der Raum beim Bearbeiten nach oben? Er
+ darf NICHT -- eine Tippfehlerkorrektur ist keine Wortmeldung. */
+ const vorher = await holen(luna.keks, "/workspace/api/chat/raeume");
+ const raumVorher = (vorher.raeume || []).find((r) => r.id === raumLunaPat);
+ ok(!!raumVorher?.letzte?.erstellt,
+ `der Raum steht in der Liste und hat einen Zeitpunkt (${raumVorher?.letzte?.erstellt || "keinen"})`);
+ const standVorher = raumVorher?.letzte?.erstellt;
+
+ /* ---- Die eigene: geht ------------------------------------------- */
+ const b = await rufe(luna.keks, `/workspace/api/chat/nachrichten/${nachricht.id}`,
+ { text: "Ich komme morgen" }, "PATCH");
+ ok(b.ok, `die eigene bearbeiten: geht (${b.status})`);
+ const bd = await b.json().catch(() => ({}));
+ ok(!!bd.bearbeitet_am, `und der Zeitpunkt kommt zurueck (${bd.bearbeitet_am || "nichts"})`);
+
+ /* ---- Steht es auch im Verlauf? ---------------------------------- */
+ const verlauf = await holen(luna.keks,
+ `/workspace/api/chat/raeume/${raumLunaPat}/nachrichten`);
+ const drin = (verlauf.nachrichten || []).find((x) => x.id === nachricht.id);
+ ok(drin?.text === "Ich komme morgen",
+ `der neue Text steht im Verlauf ("${drin?.text}")`);
+ ok(!!drin?.bearbeitet_am,
+ `und der Vermerk "bearbeitet" geht mit nach draussen (${drin?.bearbeitet_am || "fehlt"})`);
+ /* DIE SPALTE MUSS AUCH IM SELECT STEHEN. Genau das hat am 03.10.
+ schon einmal einen halben Tag gekostet: `anhang_datei` fehlte dort,
+ die Abbildung lieferte brav `null`, und alles sah gebaut aus. */
+
+ /* ---- Der Raum ist NICHT nach oben gerueckt ---------------------- */
+ const nachher = await holen(luna.keks, "/workspace/api/chat/raeume");
+ const raumNachher = (nachher.raeume || []).find((r) => r.id === raumLunaPat);
+ ok(!!standVorher && standVorher === raumNachher?.letzte?.erstellt,
+ `der Raum rueckt nicht nach oben (${standVorher} -> ${raumNachher?.letzte?.erstellt})`);
+ /* Die Vorschau in der Liste zieht aber sehr wohl nach -- sie liest
+ dieselbe Nachricht. Stuende dort weiter der Tippfehler, waere die
+ Liste eine zweite, veraltete Wahrheit neben dem Verlauf. */
+ ok(raumNachher?.letzte?.text === "Ich komme morgen",
+ `aber die Vorschau zeigt den neuen Text ("${raumNachher?.letzte?.text}")`);
+
+ /* ---- GEGENPROBE: fremde Nachricht ------------------------------- */
+ const c = await rufe(pat.keks, `/workspace/api/chat/nachrichten/${nachricht.id}`,
+ { text: "Von Patrick umgeschrieben" }, "PATCH");
+ ok(c.status === 403, `Gegenprobe: Patrick aendert sie NICHT (${c.status})`);
+
+ /* ---- GEGENPROBE, DIE ZAEHLT: auch DogFather nicht --------------- */
+ const d = await rufe(dogi.keks, `/workspace/api/chat/nachrichten/${nachricht.id}`,
+ { text: "Von DogFather umgeschrieben" }, "PATCH");
+ ok(d.status === 403 || d.status === 404,
+ `auch DogFather nicht -- loeschen darf er, umschreiben nicht (${d.status})`);
+
+ const nochda = await holen(luna.keks,
+ `/workspace/api/chat/raeume/${raumLunaPat}/nachrichten`);
+ const unberuehrt = (nochda.nachrichten || []).find((x) => x.id === nachricht.id);
+ ok(unberuehrt?.text === "Ich komme morgen",
+ `und ihr Text ist unberuehrt ("${unberuehrt?.text}")`);
+
+ /* ---- Leer und zu lang ------------------------------------------- */
+ const leer = await rufe(luna.keks, `/workspace/api/chat/nachrichten/${nachricht.id}`,
+ { text: " " }, "PATCH");
+ ok(leer.status === 400, `leer geht nicht -- dafuer gibt es loeschen (${leer.status})`);
+ const lang = await rufe(luna.keks, `/workspace/api/chat/nachrichten/${nachricht.id}`,
+ { text: "x".repeat(4001) }, "PATCH");
+ ok(lang.status === 400, `zu lang geht nicht (${lang.status})`);
+
+ /* ---- Unveraendert setzt KEINEN Vermerk -------------------------- */
+ const zweite = await rufe(luna.keks, `/workspace/api/chat/raeume/${raumLunaPat}/nachrichten`,
+ { text: "Steht schon richtig" }, "POST");
+ const n2 = (await zweite.json()).nachricht;
+ const gleich = await rufe(luna.keks, `/workspace/api/chat/nachrichten/${n2.id}`,
+ { text: "Steht schon richtig" }, "PATCH");
+ const gd = await gleich.json().catch(() => ({}));
+ ok(gleich.ok && gd.unveraendert === true && !gd.bearbeitet_am,
+ `derselbe Text setzt keinen Vermerk (${JSON.stringify(gd)})`);
+ const pruefN2 = (await holen(luna.keks,
+ `/workspace/api/chat/raeume/${raumLunaPat}/nachrichten`)).nachrichten
+ .find((x) => x.id === n2.id);
+ ok(!pruefN2?.bearbeitet_am,
+ `und an ihr steht nichts -- sonst waere der Vermerk nichts mehr wert`);
+
+ /* ---- Eine geloeschte laesst sich nicht bearbeiten --------------- */
+ await rufe(luna.keks, `/workspace/api/chat/nachrichten/${n2.id}`, undefined, "DELETE");
+ const tot = await rufe(luna.keks, `/workspace/api/chat/nachrichten/${n2.id}`,
+ { text: "Doch noch etwas" }, "PATCH");
+ ok(tot.status === 404,
+ `eine geloeschte Nachricht gibt es nicht mehr zu bearbeiten (${tot.status})`);
+
+ /* ---- Wer nicht im Raum ist, sieht sie gar nicht ----------------- */
+ const fremderRaum = await rufe(nora.keks, `/workspace/api/chat/nachrichten/${nachricht.id}`,
+ { text: "Von Nora" }, "PATCH");
+ ok(fremderRaum.status === 404,
+ `wer nicht im Raum ist, bekommt 404 und nicht 403 (${fremderRaum.status})`);
+}
+
/* =======================================================================
7. Zurücknehmen
======================================================================= */
diff --git a/server/workspace-chat.js b/server/workspace-chat.js
index 1f349311..92158ba7 100644
--- a/server/workspace-chat.js
+++ b/server/workspace-chat.js
@@ -1818,7 +1818,7 @@ chatRouter.get("/workspace/api/chat/raeume/:id/nachrichten", (req, res) => {
(Und KEINE Gegen-Apostrophe in diesem Text: Er steht
in einem Template-Literal, und einer davon beendet
es. Heute schon das zweite Mal.) */
- n.anhang_datei,
+ n.anhang_datei, n.bearbeitet_am,
a.text AS zitat_text, a.weg_am AS zitat_weg, ap.name AS zitat_von,
a.anhang_art AS zitat_anhang
FROM chat_nachrichten n
@@ -2034,6 +2034,13 @@ chatRouter.get("/workspace/api/chat/raeume/:id/nachrichten", (req, res) => {
von_kachel: kachelPruefen(n.von_kachel) || autoKachelFuer(n.person_id),
text: n.weg_am ? null : n.text,
zurueckgenommen: !!n.weg_am,
+ /* WANN BEARBEITET -- oder null. Geht nach draussen, weil an
+ der Nachricht "bearbeitet" stehen soll: Ein Text, der sich
+ hinterher still aendert, waehrend andere schon darauf
+ geantwortet haben, ist ein Vertrauensproblem und kein
+ Komfort. An einer geloeschten steht es nicht, dort gibt es
+ keinen Text mehr, auf den es sich beziehen koennte. */
+ bearbeitet_am: n.weg_am ? null : (n.bearbeitet_am || null),
erstellt: n.erstellt,
selbst: n.person_id === req.person.id,
angeheftet: !!n.angeheftet_am,
@@ -3597,6 +3604,102 @@ chatRouter.put("/workspace/api/chat/nachrichten/:id/anheften", gleicheHerkunft,
const darfJedeNachrichtLoeschen = (person) =>
istDogFather(person) || person?.rolle === "hand";
+/* =====================================================================
+ EIGENE NACHRICHT BEARBEITEN (03.10.2026)
+
+ Filipe: "die nachrichten die man in den chat reinschreibt. jeder
+ soll seine eigene nachricht bearbeiten koennen. diese option soll
+ jeder fuer seine eigene nachrichten haben die er selber verfasst
+ hat."
+
+ NUR DIE EIGENE -- OHNE AUSNAHME, auch nicht fuer DogFather und die
+ rechte Hand. Beim LOESCHEN gibt es diese Ausnahme (siehe
+ `darfJedeNachrichtLoeschen`, fuer den Notfall), und es waere
+ naheliegend, sie hier mitzunehmen. Das waere falsch: Eine fremde
+ Nachricht zu entfernen heisst "das soll hier nicht stehen". Eine
+ fremde Nachricht zu AENDERN heisst, jemandem Worte in den Mund zu
+ legen, die unter seinem Namen und seinem Bild stehen bleiben. Das
+ ist nicht dieselbe Befugnis in groesser, sondern eine andere.
+
+ ERWAEHNUNGEN BLEIBEN, WIE SIE BEIM SENDEN WAREN. Wer beim
+ Bearbeiten "@Anna" ergaenzt, spricht Anna damit NICHT an. Der
+ Grund ist die Richtung, in die es sonst schiefgeht: Entweder man
+ benachrichtigt nachtraeglich -- dann laesst sich jemand beliebig
+ oft anpingen, indem man dieselbe Nachricht immer wieder
+ bearbeitet -- oder man traegt die Erwaehnung still ein, und dann
+ steht jemand auf einer Liste, von der er nie erfaehrt. Beides ist
+ schlechter als die klare Regel: Ansprechen tut man mit einer neuen
+ Nachricht.
+
+ DER RAUM RUECKT NICHT NACH OBEN, und niemand bekommt sie als
+ ungelesen. Ein Bearbeiten ist keine Wortmeldung; wuerde der Raum
+ dadurch hochspringen, waere jede Tippfehlerkorrektur eine Stoerung
+ fuer alle anderen. Genau deshalb wird `letzte_am` hier NICHT
+ angefasst -- anders als beim Loeschen, wo es neu gerechnet werden
+ muss, weil die juengste Nachricht verschwunden sein kann.
+ ===================================================================== */
+chatRouter.patch("/workspace/api/chat/nachrichten/:id", gleicheHerkunft,
+ express.json({ limit: "8kb" }), (req, res) => {
+ try {
+ const id = Number(req.params.id);
+ if (!Number.isInteger(id)) return res.status(400).json({ fehler: "ungueltig" });
+
+ /* DIESELBEN GRENZEN WIE BEIM SENDEN. Eine zweite Rechnung waere
+ die, die auseinanderlaeuft -- und ein Text, der sich senden,
+ aber nicht bearbeiten laesst, waere eine Falle. */
+ const text = String(req.body?.text ?? "").trim();
+ if (!text) {
+ /* LEEREN IST LOESCHEN, und dafuer steht der Knopf daneben. Wer
+ hier eine leere Nachricht zuliesse, haette zwei Wege zu
+ derselben Sache -- und einen davon ohne Rueckfrage. */
+ return res.status(400).json({ fehler: "leer" });
+ }
+ if (text.length > TEXT_MAX) {
+ return res.status(400).json({ fehler: `Länger als ${TEXT_MAX} Zeichen geht nicht.` });
+ }
+
+ const d = db();
+ const n = d.prepare(
+ "SELECT id, raum_id, person_id, text, weg_am FROM chat_nachrichten WHERE id = ?").get(id);
+ /* Was jemand nicht sehen darf, gibt es fuer ihn nicht: 404 statt
+ 403 -- dieselbe Ueberlegung wie beim Loeschen eine Route
+ weiter unten. */
+ if (!n || !istDrin(n.raum_id, req.person)) {
+ return res.status(404).json({ fehler: "nicht_gefunden" });
+ }
+ if (n.person_id !== req.person.id) {
+ return res.status(403).json({ fehler: "Nur eigene Nachrichten." });
+ }
+ if (n.weg_am) {
+ return res.status(409).json({ fehler: "Diese Nachricht ist zurückgenommen." });
+ }
+ /* UNVERAENDERT IST KEINE AENDERUNG. Sonst stuende "bearbeitet" an
+ einer Nachricht, an der niemand etwas getan hat -- und der
+ Vermerk waere ab da nichts mehr wert. */
+ if (text === n.text) {
+ return res.json({ ok: true, unveraendert: true });
+ }
+
+ const wann = new Date().toISOString();
+ d.prepare("UPDATE chat_nachrichten SET text = ?, bearbeitet_am = ? WHERE id = ?")
+ .run(text, wann, id);
+
+ /* An ALLE im Raum, aber an niemanden als Benachrichtigung (leere
+ Empfaengerliste) -- siehe oben. */
+ chatEreignis(n.raum_id,
+ { id, raum_id: n.raum_id, text, bearbeitet_am: wann }, [], "geaendert");
+
+ protokolliere("chat_bearbeitet", {
+ personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
+ detail: `#${id}`,
+ });
+ res.json({ ok: true, bearbeitet_am: wann, text });
+ } catch (fehler) {
+ console.error("[chat] Bearbeiten:", fehler?.message);
+ res.status(503).json({ fehler: "nicht_verfuegbar" });
+ }
+});
+
chatRouter.delete("/workspace/api/chat/nachrichten/:id", gleicheHerkunft, (req, res) => {
try {
const id = Number(req.params.id);
diff --git a/server/workspace.js b/server/workspace.js
index eb72131d..c9bd74ac 100644
--- a/server/workspace.js
+++ b/server/workspace.js
@@ -4038,6 +4038,12 @@ function umstellungen(d) {
sobald es die Datei kennt. Was wirklich schuetzt, ist die
Byte-Grenze, nicht diese Zahl. */
["chat_nachrichten", "anhang_dauer", "INTEGER"],
+ /* WANN ZULETZT BEARBEITET (03.10.2026) -- NULL heisst: nie.
+ Filipe: "jeder soll seine eigene nachricht bearbeiten koennen."
+ Die Spalte traegt die Zeit und nicht nur ein Ja/Nein: "vor zwei
+ Minuten geaendert" und "gestern geaendert" sind zwei
+ verschiedene Auskuenfte, und die zweite will man sehen. */
+ ["chat_nachrichten", "bearbeitet_am", "TEXT"],
/* WELCHE VORLAGE DAHINTERSTECKT (09.09.2026).
diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html
index ce6bd5b8..f8346fe3 100644
--- a/workspace/anruf-probe.html
+++ b/workspace/anruf-probe.html
@@ -16,8 +16,8 @@
verdeckt. Gefunden hat es pruef-struktur.
Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. -->
-
-
+
+
-
+