From b625d901a5a0b68bb24b853b025ad0364693d4ee Mon Sep 17 00:00:00 2001 From: Dogfather Date: Sun, 4 Oct 2026 00:06:29 +0200 Subject: [PATCH] Jeder darf seine eigene Chat-Nachricht bearbeiten 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 seit dem 22.09. (fuer den Notfall), und es waere naheliegend gewesen, sie mitzunehmen. Das waere falsch: Eine fremde Nachricht zu entfernen heisst "das soll hier nicht stehen". Eine fremde zu AENDERN heisst, jemandem Worte in den Mund zu legen, die unter seinem Namen und seinem Bild stehen bleiben. Nicht dieselbe Befugnis in groesser, sondern eine andere. Genau das ist die wichtigste Pruefzeile: DogFather darf loeschen und bekommt beim Bearbeiten 403/404. AN DER NACHRICHT STEHT "BEARBEITET". Ein Text, der sich still aendert, nachdem andere darauf geantwortet haben, ist ein Vertrauensproblem und kein Komfort. Der Vermerk traegt die Zeit im Titel. Derselbe Text setzt ihn NICHT -- sonst stuende er irgendwann ueberall und waere nichts mehr wert. ERWAEHNUNGEN BLEIBEN, WIE SIE BEIM SENDEN WAREN. Wer beim Bearbeiten "@Anna" ergaenzt, spricht Anna damit nicht an. Sonst gaebe es nur schlechte Wege: nachtraeglich benachrichtigen laesst sich beliebig oft wiederholen, und still eintragen setzt jemanden auf eine Liste, von der er nie erfaehrt. Ansprechen tut man mit einer neuen Nachricht. (Falls das anders gewuenscht ist, ist es eine eigene Entscheidung -- nicht etwas, das hier nebenbei mitpassiert.) DER RAUM RUECKT NICHT NACH OBEN und niemand bekommt die Nachricht als ungelesen: Eine Tippfehlerkorrektur ist keine Wortmeldung. `letzte_am` wird deshalb nicht angefasst. Leer geht nicht -- dafuer steht "loeschen" daneben, mit Rueckfrage. Grenzen (4000 Zeichen) sind dieselben wie beim Senden; eine zweite Rechnung waere die, die auseinanderlaeuft. Das Feld sitzt AN der Nachricht, nicht im Schreibfeld unten: Wer seinen Text zum Bearbeiten unten wiederfindet, schickt ihn beim naechsten Enter als NEUE Nachricht ab und hat ihn zweimal im Raum. Enter speichert, Shift+Enter macht eine Zeile, Escape bricht ab -- dieselben Tasten wie beim Schreiben. 16 px Schrift, sonst zoomt iOS beim Hineintippen die ganze Seite heran. Der Stift traegt sich in die Familie der Handgriffe ein, wie es der Hinweis in chat.css ausdruecklich verlangt ("wer einen sechsten Handgriff baut, traegt ihn hier ein und bekommt sein Zeichen"). EIN FEHLER, DEN NUR DAS BILD GEZEIGT HAT. Ich hatte im Code behauptet, die Gespraechsliste aendere sich beim Bearbeiten nicht, und darum auf das Nachladen verzichtet. Auf dem Bildschirmfoto stand rechts "Treffen um 15 Uhr" und links in der Liste weiter "Du: Treffen um 15 Urh" -- derselbe Satz, zweimal verschieden, auf einem Schirm. Richtig ist: Die REIHENFOLGE aendert sich nicht, die VORSCHAU sehr wohl. Beide Haelften waren fuer sich gemessen und gruen; keine Zahl hat es gemerkt. pruef-chat 17 neue Pruefungen: eigene geht, fremde nicht, DogFather nicht, Vermerk kommt mit nach draussen (auch im SELECT -- genau das hat am 03.10. bei den Anhaengen einen halben Tag gekostet), Raum rueckt nicht, Vorschau zieht nach, leer/zu lang abgelehnt, unveraendert ohne Vermerk, geloeschte nicht bearbeitbar, wer nicht im Raum ist bekommt 404 statt 403. pruef-chat-optik 71 -> 84: im echten Browser, mit zwei Sitzungen. Darunter die Zeile, auf die es ankommt -- der neue Text steht bei Patrick, OHNE Neuladen. Ein Bearbeiten, das nur der Schreibende sieht, waere schlimmer als keins. Dabei zwei eigene Messfehler behoben: Die Sitzungen von oben waren laengst geschlossen (Playwright meldet nur "Target page has been closed"), und beide klickten "das oberste Gespraech" statt denselben Raum -- wodurch die Pruefung "an ihr steht KEIN bearbeiten" gruen war, weil die Nachricht gar nicht da war. Sie haengt jetzt daran, dass er sie wirklich sieht. Datenbank vorher gesichert und zurueckgelesen (integrity_check, 324 Nachrichten, 20 Personen). Co-Authored-By: Claude Opus 5 --- server/pruef-chat-optik.mjs | 151 +++++++++++++++++++++++++++ server/pruef-chat.mjs | 115 +++++++++++++++++++++ server/workspace-chat.js | 105 ++++++++++++++++++- server/workspace.js | 6 ++ workspace/anruf-probe.html | 10 +- workspace/app.webmanifest | 8 +- workspace/assets/css/chat.css | 79 ++++++++++++++ workspace/assets/js/chat.js | 176 +++++++++++++++++++++++++++++++- workspace/aufgaben.html | 38 +++---- workspace/automation.html | 32 +++--- workspace/befinden.html | 34 +++--- workspace/bereich.html | 44 ++++---- workspace/bewerben.html | 38 +++---- workspace/bewerbungen.html | 36 +++---- workspace/buehne.html | 6 +- workspace/calls.html | 34 +++--- workspace/chat.html | 40 ++++---- workspace/content.html | 34 +++--- workspace/crew-index.html | 12 +-- workspace/crew.webmanifest | 8 +- workspace/dateien.html | 34 +++--- workspace/entwicklung.html | 36 +++---- workspace/hilfe.html | 30 +++--- workspace/index.html | 10 +- workspace/kalender.html | 34 +++--- workspace/leistung.html | 32 +++--- workspace/manager-ziele.html | 28 ++--- workspace/material.html | 32 +++--- workspace/notizen.html | 28 ++--- workspace/personen.html | 32 +++--- workspace/profil.html | 36 +++---- workspace/reaktion.html | 38 +++---- workspace/rechte.html | 34 +++--- workspace/report.html | 36 +++---- workspace/scouting.html | 32 +++--- workspace/start.html | 32 +++--- workspace/startcheck.html | 34 +++--- workspace/steckbrief.html | 36 +++---- workspace/support.html | 28 ++--- workspace/tafel.html | 6 +- workspace/talente.html | 36 +++---- workspace/team.html | 32 +++--- workspace/teamlage.html | 34 +++--- workspace/teilen.html | 30 +++--- workspace/treff-moderation.html | 32 +++--- workspace/treff-regeln.html | 32 +++--- workspace/uebersicht.html | 32 +++--- workspace/unsere-seiten.html | 30 +++--- workspace/unterstuetzen.html | 32 +++--- workspace/werdegang.html | 32 +++--- workspace/willkommen.html | 30 +++--- workspace/wissen.html | 34 +++--- 52 files changed, 1314 insertions(+), 686 deletions(-) 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`. --> - - + + - +