diff --git a/server/pruef-chat.mjs b/server/pruef-chat.mjs
index 38c64825..89549df3 100644
--- a/server/pruef-chat.mjs
+++ b/server/pruef-chat.mjs
@@ -402,6 +402,110 @@ console.log("\n=== Ohne Anmeldung ===");
d.close();
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
+/* =====================================================================
+ GESPRAECHE OBEN ANHEFTEN (25.09.2026)
+ =====================================================================
+
+ Filipe: „ich will dass man auch individuel jeder fuer sich auch in
+ der liste chats fixieren kann. auch mehrere nicht nur eins."
+
+ DREI AUSSAGEN STECKEN IN DIESEM SATZ, und jede wird einzeln
+ geprueft:
+
+ „fixieren" -> das Angeheftete steht OBEN, nicht irgendwo
+ „individuell" -> beim anderen aendert sich NICHTS
+ „auch mehrere" -> zwei gleichzeitig, und die Reihenfolge stimmt
+
+ DIE ZWEITE IST DIE WICHTIGSTE und die, die am leichtesten kaputt
+ geht: Eine Spalte an `chat_raeume` haette alles andere genauso
+ erfuellt -- und jedem im Raum das Gespraech oben hingeklebt. Man
+ haette es erst gemerkt, wenn sich jemand beschwert.
+
+ GEGENPROBEN: fremder Raum (404) und das Loesen (steht wieder
+ unten). Ohne sie waere „steht oben" ein Haken, der auch gruen
+ waere, wenn die Liste immer dieselbe Reihenfolge haette.
+ ===================================================================== */
+console.log("\n=== Anheften: oben, einzeln, mehrere ===");
+{
+ /* Zwei eigene Raeume anlegen -- so haengt die Pruefung nicht davon
+ ab, wie viele Gespraeche zufaellig schon dastehen. Mit Luna
+ zusammen, denn an ihr wird gleich gezeigt, dass sich bei ihr
+ nichts aendert. */
+ const machGruppe = async (name) => {
+ const a = await rufe(dogi.keks, "/workspace/api/chat/gruppe",
+ { name, mit: [idLuna, idPat] }, "POST");
+ const j = await a.json().catch(() => ({}));
+ return j.raum_id;
+ };
+ const raumA = await machGruppe("Anheft-Probe A");
+ const raumB = await machGruppe("Anheft-Probe B");
+ ok(Number.isInteger(raumA) && Number.isInteger(raumB) && raumA !== raumB,
+ `zwei Raeume zum Anheften (${raumA}, ${raumB})`);
+
+ const liste = async (keks) => (await holen(keks, "/workspace/api/chat/raeume")).raeume || [];
+ const anheften = (keks, raum, an) =>
+ rufe(keks, `/workspace/api/chat/raeume/${raum}/fixieren`, { an }, "PUT");
+
+ /* VORHER: nichts ist angeheftet. Ohne diese Zeile koennte alles
+ Folgende auch dann gruen sein, wenn die Spalte von sich aus
+ gefuellt waere. */
+ const vorher = await liste(dogi.keks);
+ ok(vorher.length >= 2 && vorher.every((r) => r.fixiert === false),
+ `vorher ist nichts angeheftet (${vorher.length} Gespraeche)`);
+
+ /* ---- 1. „fixieren": oben ---------------------------------------- */
+ const wo = vorher.findIndex((r) => r.id === raumA);
+ const a1 = await anheften(dogi.keks, raumA, true);
+ const j1 = await a1.json().catch(() => ({}));
+ ok(a1.status === 200 && j1.fixiert === true,
+ `Raum A laesst sich anheften (HTTP ${a1.status}, fixiert ${j1.fixiert})`);
+ const nachA = await liste(dogi.keks);
+ ok(nachA[0]?.id === raumA,
+ `und steht danach ganz oben (vorher an Stelle ${wo + 1}, jetzt an 1)`);
+ ok(nachA[0]?.fixiert === true, " und traegt die Marke dafuer");
+
+ /* ---- 2. „individuell": bei Luna aendert sich nichts -------------- */
+ const beiLuna = await liste(luna.keks);
+ const lunaA = beiLuna.find((r) => r.id === raumA);
+ ok(!!lunaA && lunaA.fixiert === false,
+ `Gegenprobe: bei Luna ist derselbe Raum NICHT angeheftet (${lunaA?.fixiert})`);
+ /* UND ER STEHT BEI IHR AUCH NICHT OBEN. Die Marke allein wuerde
+ nicht reichen: Sortiert der Server nach einer Spalte am RAUM,
+ stuende er bei ihr oben, obwohl `fixiert` false meldet. */
+ ok(beiLuna.length < 2 || beiLuna[0].id !== raumA
+ || beiLuna.filter((r) => r.fixiert).length > 0,
+ ` und rutscht bei ihr auch nicht nach oben (oben steht ${beiLuna[0]?.id})`);
+
+ /* ---- 3. „auch mehrere" ------------------------------------------ */
+ await anheften(dogi.keks, raumB, true);
+ const nachB = await liste(dogi.keks);
+ ok(nachB.filter((r) => r.fixiert).length === 2,
+ `zwei gleichzeitig angeheftet (${nachB.filter((r) => r.fixiert).length})`);
+ ok(nachB[0]?.id === raumB && nachB[1]?.id === raumA,
+ `und das zuletzt angeheftete steht oben (${nachB[0]?.id}, dann ${nachB[1]?.id})`);
+ ok(nachB.slice(2).every((r) => r.fixiert === false),
+ " darunter geht die gewohnte Reihenfolge weiter");
+
+ /* ---- 4. Loesen --------------------------------------------------- */
+ const a2 = await anheften(dogi.keks, raumB, false);
+ ok(a2.status === 200, `es laesst sich wieder loesen (HTTP ${a2.status})`);
+ const nachLos = await liste(dogi.keks);
+ ok(nachLos[0]?.id === raumA && nachLos.filter((r) => r.fixiert).length === 1,
+ `danach haengt nur noch A oben (oben ${nachLos[0]?.id}, `
+ + `${nachLos.filter((r) => r.fixiert).length} angeheftet)`);
+
+ /* ---- 5. Ein fremder Raum ---------------------------------------- */
+ /* Nora sitzt in keinem der beiden. Kaeme hier etwas anderes als 404
+ zurueck, liesse sich aus der Antwort ablesen, welche Raumnummern
+ es ueberhaupt gibt. */
+ const fremd = await anheften(nora.keks, raumA, true);
+ ok(fremd.status === 404,
+ `Gegenprobe: ein fremder Raum laesst sich nicht anheften (${fremd.status})`);
+ const beiNora = await liste(nora.keks);
+ ok(beiNora.every((r) => r.fixiert === false),
+ " und bei ihr haengt danach auch nichts");
+}
+
console.log(`\n${fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`}`);
/* HIER IST process.exit() RICHTIG -- anders als in pruef-push-weg.
diff --git a/server/workspace-chat.js b/server/workspace-chat.js
index 06c8e7ed..dc696de4 100644
--- a/server/workspace-chat.js
+++ b/server/workspace-chat.js
@@ -1110,7 +1110,7 @@ chatRouter.get("/workspace/api/chat/raeume", (req, res) => {
angelegt hat. */
const reihen = db().prepare(`
SELECT r.id, r.art, r.name, r.kategorie, r.letzte_am, r.erstellt_von,
- t.gelesen_bis, t.leitung, t.geloescht_bis
+ t.gelesen_bis, t.leitung, t.geloescht_bis, t.fixiert_am
FROM chat_raeume r
JOIN chat_teilnehmer t ON t.raum_id = r.id AND t.person_id = ?
WHERE t.raus_am IS NULL
@@ -1118,7 +1118,19 @@ chatRouter.get("/workspace/api/chat/raeume", (req, res) => {
AND (t.geloescht_am IS NULL
OR EXISTS (SELECT 1 FROM chat_nachrichten n
WHERE n.raum_id = r.id AND n.id > t.geloescht_bis))
- ORDER BY CASE WHEN r.letzte_am IS NULL THEN 1 ELSE 0 END, r.letzte_am DESC, r.id DESC`)
+ /* ANGEHEFTETES ZUERST (25.09.2026), und INNERHALB der beiden
+ Bloecke bleibt die gewohnte Ordnung: zuletzt gesprochen oben.
+ Wer etwas anheftet, will es oben haben -- er will aber nicht,
+ dass sich die Reihenfolge darunter aendert.
+
+ ZULETZT ANGEHEFTET STEHT GANZ OBEN. Eine andere Regel gaebe
+ es nicht: Nach Name waere es Zufall, nach Datum der letzten
+ Nachricht waere das Anheften wirkungslos, sobald jemand
+ schreibt. */
+ ORDER BY CASE WHEN t.fixiert_am IS NULL THEN 1 ELSE 0 END,
+ t.fixiert_am DESC,
+ CASE WHEN r.letzte_am IS NULL THEN 1 ELSE 0 END,
+ r.letzte_am DESC, r.id DESC`)
.all(req.person.id);
const raeume = reihen.map((r) => {
@@ -1203,6 +1215,10 @@ chatRouter.get("/workspace/api/chat/raeume", (req, res) => {
} : null,
ungelesen,
erwaehnt,
+ /* Als Ja/Nein an die Oberflaeche -- WANN jemand etwas
+ angeheftet hat, geht niemanden etwas an, auch ihn selbst
+ nicht. Der Zeitstempel ist Sortierung, keine Auskunft. */
+ fixiert: !!r.fixiert_am,
};
});
/* WELCHE KANAELE ES NOCH NICHT GIBT -- und nur fuer die, die sie
@@ -2985,6 +3001,52 @@ chatRouter.put("/workspace/api/chat/raeume/:id/gelesen", gleicheHerkunft,
}
});
+/* =====================================================================
+ EIN GESPRAECH OBEN ANHEFTEN (25.09.2026)
+ =====================================================================
+
+ Filipe: „ich will dass man auch individuel jeder fuer sich auch in
+ der liste chats fixieren kann. auch mehrere nicht nur eins."
+
+ JEDER FUER SICH: Geschrieben wird in die TEILNEHMER-Zeile, nicht an
+ den Raum. Wer den Treff anheftet, heftet ihn nur bei sich an -- eine
+ Spalte am Raum waere eine Entscheidung fuer alle acht Leute darin.
+
+ BELIEBIG VIELE: Es gibt keine Obergrenze und keine "erst das alte
+ loesen"-Logik. Er hat ausdruecklich „auch mehrere" gesagt, und eine
+ Grenze waere eine Zahl, die irgendwann jemand neu raten muesste.
+
+ `istDrin` UND NICHT NUR "gibt es den Raum": Sonst liesse sich aus
+ der Antwort ablesen, welche Raumnummern es ueberhaupt gibt -- und
+ ein 404 fuer beides (nicht da / nicht meiner) sagt am wenigsten.
+
+ DIE ANTWORT TRAEGT DEN NEUEN STAND und nicht nur „ok". Die
+ Oberflaeche zeichnet die Liste danach ohnehin neu; der Stand in der
+ Antwort ist die Sicherung dagegen, dass sie ihn selbst ausrechnet
+ und dabei danebenliegt. */
+chatRouter.put("/workspace/api/chat/raeume/:id/fixieren", gleicheHerkunft,
+ express.json({ limit: "4kb" }), (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" });
+
+ /* AUSDRUECKLICH `=== true` / `=== false` und kein Umschalten
+ ohne Angabe. Ein Schalter, der den Gegenwert selbst ausrechnet,
+ kippt bei zwei schnellen Klicks oder zwei offenen Fenstern in
+ den falschen Zustand -- und niemand sieht, warum. Die
+ Oberflaeche weiss, was sie will; sie sagt es. */
+ const an = req.body?.an === true;
+ db().prepare(`UPDATE chat_teilnehmer SET fixiert_am = ?
+ WHERE raum_id = ? AND person_id = ?`)
+ .run(an ? new Date().toISOString() : null, raumId, req.person.id);
+ res.json({ ok: true, fixiert: an });
+ } catch (fehler) {
+ console.error("[chat] Anheften:", fehler?.message);
+ res.status(503).json({ fehler: "nicht_verfuegbar" });
+ }
+ });
+
/* =====================================================================
ANKUENDIGUNGEN ANHEFTEN (10.09.2026, Kapitel 7.2)
diff --git a/server/workspace.js b/server/workspace.js
index b87bce57..63c9d498 100644
--- a/server/workspace.js
+++ b/server/workspace.js
@@ -3829,6 +3829,23 @@ function umstellungen(d) {
["chat_teilnehmer", "geloescht_bis", "INTEGER NOT NULL DEFAULT 0"],
["chat_teilnehmer", "geloescht_am", "TEXT"],
+ /* GESPRAECHE OBEN ANHEFTEN (25.09.2026).
+
+ Filipe: „ich will dass man auch individuel jeder fuer sich auch
+ in der liste chats fixieren kann. auch mehrere nicht nur eins."
+
+ AN DER TEILNEHMERZEILE UND NICHT AM RAUM -- das ist der ganze
+ Punkt seines Satzes: „individuell jeder fuer sich". Eine Spalte
+ an `chat_raeume` waere eine Entscheidung FUER ALLE; wer den
+ Treff anheftet, haette ihn jedem anderen oben hingeklebt.
+
+ EIN ZEITSTEMPEL UND KEIN JA/NEIN. Er kostet dasselbe und
+ beantwortet eine Frage mehr: In welcher Reihenfolge stehen
+ mehrere angeheftete Gespraeche? Zuletzt angeheftet steht oben --
+ das ist die Reihenfolge, die man selbst gebaut hat. Mit einer
+ 0/1-Spalte muesste man sie erfinden. */
+ ["chat_teilnehmer", "fixiert_am", "TEXT"],
+
/* ANHAENGE: FOTOS UND PDF (09.09.2026).
Filipe: "am besten waere es auch wenn man da auch im chat pdfs
diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html
index adc5f77b..e154979a 100644
--- a/workspace/anruf-probe.html
+++ b/workspace/anruf-probe.html
@@ -16,15 +16,15 @@
verdeckt. Gefunden hat es pruef-struktur.
Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. -->
-
-
+
+
-
+