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`. --> - - + + - +