Chat: Fotos und PDFs schicken, Gespraeche wegraeumen

Filipe, mit Bildschirmfoto: "man muss die chats auch geloescht
bekommen!!! am besten waere es auch wenn man da auch im chat pdfs
schicken koennte. pdfs und fotos."

Zwei Entscheidungen vorher abgestimmt, weil sie den Bau bestimmen:
Geloescht wird NUR BEI MIR. Schicken duerfen ALLE im Gespraech, auch
Creator -- anders als in der Dateiablage, wo etwas in einem Bereich
landet, den mehrere sehen. Hier bekommt es genau der, mit dem man
ohnehin gerade spricht.

--- ANHAENGE ---

Drei Wege zur selben Sache, weil Leute unterschiedlich arbeiten: die
Bueroklammer, Hineinziehen und Einfuegen mit Strg+V (der Weg fuer einen
Screenshot -- der liegt in der Zwischenablage und nirgends als Datei).
Alle drei laufen durch dieselbe Funktion; drei Fassungen waeren drei
Gelegenheiten, dass eine die Pruefung vergisst.

Ein Foto wird GEZEIGT, ein PDF wird als Karte ANGEBOTEN. Das ist kein
Schoenheitsunterschied: Ein Bild erkennt man in einer Zehntelsekunde,
ein PDF muss man ohnehin oeffnen -- eine Vorschau davon waere ein
grauer Kasten, der so tut, als koennte man etwas lesen.

DIE SICHERHEIT STEHT IN dateiErkennen(). Dateiname und Content-Type
kommen vom Absender und sind frei erfunden; der Typ wird deshalb aus
den ersten Bytes bestimmt (PNG/GIF/WebP/JPEG/PDF, mit Breite und Hoehe
im selben Durchgang). SVG steht absichtlich NICHT in der Liste -- das
ist XML, das Skripte enthalten darf, ein als Bild getarntes Programm.
Ausgeliefert wird spaeter genau der ERKANNTE Typ, nie der eingeschickte,
dazu nosniff und "default-src 'none'; sandbox".

Auf der Platte bekommt jede Datei einen erzeugten Zufallsnamen, in
einem Ordner ausserhalb des Repos. Zuruecknehmen loescht die Datei
wirklich -- sonst waere sie im Verlauf weg und ueber die Adresse noch
da, also nur der Anschein einer Ruecknahme.

--- WEGRAEUMEN ---

Zwei Spalten in chat_teilnehmer statt einer, wegen eines Randfalls: Bei
einem Gespraech ohne jede Nachricht waere die Grenze 0 -- und 0 heisst
sonst "nie geloescht". geloescht_am unterscheidet die beiden.

Die Grenze wirkt in der Liste, im Verlauf, in der Vorschau, in der
SUCHE, bei den Anhaengen und in der Ungelesen-Zahl. Die Suche ist die
Stelle, an der so etwas typischerweise durchrutscht: aus der Liste weg,
ueber die Suche noch da.

--- DER FUND: 323 PIXEL ---

Ein Bild ist spaeter fertig als der Rest der Seite. Beim Oeffnen springt
der Verlauf ans Ende, dann kommt das Foto an und schiebt alles darunter
weg -- man steht ploetzlich mittendrin, ohne etwas angeklickt zu haben.

Das width/height-Attribut am <img>, wie man es ueberall liest, hilft
hier NICHT: Es gibt nur das Seitenverhaeltnis. Damit daraus eine Hoehe
wird, muss die Breite feststehen -- bei "width: auto" und einem nicht
geladenen Bild ist sie 0. Der Platz wird jetzt am KASTEN reserviert
(aspect-ratio + max-width aus den gemessenen Massen).

Und die Pruefung dazu hatte denselben Fehler wie das Auge: Sie mass in
einem Fenster, in dem das Bild laengst im Zwischenspeicher lag, und
meldete tadellose 0 px. Sie misst jetzt in einem FRISCHEN Browser --
der Fall, um den es geht, ist der erste.

--- SICHERUNG ---

chat-anhaenge/ ist im selben Zug in tools/sicherung-holen.sh
eingetragen, nicht spaeter, wenn es auffaellt: Genau so ist die Luecke
bei den Profilbildern entstanden. tools/wiederherstellung-proben.mjs
bestaetigt: "der Server benutzt 4 Datenordner", keiner fehlt.

--- Pruefung ---

server/pruef-chat-anhaenge.mjs, neu, 60 Pruefungen, alle gruen.
Abschnitt 3 versucht ausdruecklich, hereinzukommen: HTML als bild.png,
SVG als foto.png, SVG als svg, eine Textdatei, ein PNG-Kopf ohne Inhalt
-- alle fuenf mit 415 abgewiesen, ein echtes JPEG danach mit 201
angenommen (sonst bewiese der Block nur, dass gar nichts durchkommt).
13 MB geben 413 mit lesbarem Text, nicht 500.

Konsolenfehler werden nach ZEITPUNKT getrennt, nicht nach Statusnummer:
Was waehrend eines absichtlichen Versuchs entsteht, ist gewollt. Nach
Nummer zu filtern waere bequem und wuerde ab morgen echte 404 mit
verschlucken.

Ausserdem gruen: pruef-chat, pruef-chat-optik, pruef-chat-ausbau,
pruef-css-klassen. Angesehen bei 1440 px und bei 390 px.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-09 12:51:54 +02:00
co-authored by Claude Opus 5
parent e28724564a
commit 14e9bbf7a5
25 changed files with 1795 additions and 212 deletions
+149
View File
@@ -927,3 +927,152 @@
@media (forced-colors: active) {
.emoji-tafel, .chat__zum-neuesten { border: 1px solid CanvasText; background: Canvas; }
}
/* =====================================================================
ANHÄNGE, ABLAGE UND WEGRÄUMEN (09.09.2026)
Filipe: "man muss die chats auch gelöscht bekommen!!! am besten wäre
es auch wenn man da auch im chat pdfs schicken könnte. pdfs und
fotos."
===================================================================== */
/* Der Wegräum-Knopf sitzt im Kopf, rechts. Er ist absichtlich LEISE:
Löschen ist die seltene Handlung, und ein roter Knopf, der jeden Tag
danebensteht, wird irgendwann aus Versehen getroffen. Rot wird er
erst beim Darüberfahren -- dann, wenn jemand ihn wirklich meint. */
.chat__weg {
flex: none; margin-left: auto;
width: 34px; height: 34px;
display: grid; place-items: center;
border: 1px solid var(--rand); border-radius: 10px;
background: rgba(255, 255, 255, .03);
color: var(--text-leise); cursor: pointer;
transition: color var(--tempo), border-color var(--tempo);
}
.chat__weg svg {
width: 17px; height: 17px;
fill: none; stroke: currentColor; stroke-width: 1.7;
stroke-linecap: round; stroke-linejoin: round;
}
.chat__weg:hover { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 45%, transparent); }
.chat__weg:focus-visible { outline: 2px solid var(--akzent); outline-offset: 2px; }
/* Steht der Knopf da, braucht der Text daneben seinen eigenen Rand --
sonst hat .chat__kopf-text keine Grenze und schiebt ihn hinaus. */
.chat__kopf--verlauf .chat__kopf-text { min-width: 0; flex: 1; }
/* ---- Die Büroklammer ------------------------------------------------ */
.chat__anhang-feld {
/* Nicht display:none -- ein verstecktes Feld lässt sich in manchen
Browsern nicht mehr über click() öffnen. Es steht da, nur ohne
Fläche. */
position: absolute; width: 1px; height: 1px;
padding: 0; margin: -1px; overflow: hidden;
clip-path: inset(50%); border: 0;
}
.chat__anhang {
flex: none;
width: 40px; height: 40px;
display: flex; align-items: center; justify-content: center;
appearance: none; border: 1px solid rgba(150, 186, 220, .18);
border-radius: 11px;
background: rgba(255, 255, 255, .04);
color: var(--text-leise); cursor: pointer;
transition: color var(--tempo), border-color var(--tempo);
}
.chat__anhang svg {
width: 19px; height: 19px;
fill: none; stroke: currentColor; stroke-width: 1.7;
stroke-linecap: round; stroke-linejoin: round;
}
.chat__anhang:hover { color: var(--text); border-color: rgba(150, 186, 220, .34); }
.chat__anhang:focus-visible { outline: 2px solid var(--akzent); outline-offset: 2px; }
.chat__anhang:disabled { opacity: .45; cursor: progress; }
/* ---- Ein Foto in der Blase ------------------------------------------ */
/* Die Höhe ist begrenzt, die Breite folgt dem Bild. Ein Hochformat
nähme sonst den halben Verlauf ein und schöbe alles darunter aus dem
Bild -- man scrollt dann durch ein Foto statt durch ein Gespräch. */
.chat-bild {
display: block;
width: 100%; /* die Hoechstbreite setzt das Skript */
margin: 0 0 6px;
border-radius: 12px;
overflow: hidden;
line-height: 0;
background: rgba(255, 255, 255, .04);
}
/* Die Groesse macht der Kasten (aspect-ratio + max-width, siehe
chat.js). Das Bild fuellt ihn nur aus. Stuende die Groesse hier am
Bild, waere sie erst bekannt, wenn das Bild da ist -- und genau das
war der Fehler, der den Verlauf um 323 px verschoben hat. */
.chat-bild img {
display: block;
width: 100%; height: 100%;
object-fit: contain;
}
/* Ohne bekannte Masse gibt es kein Verhaeltnis, an dem der Kasten
haengen koennte. Dann greift der alte Weg -- und der Zuhoerer auf
`load` in chat.js haelt den Bildlauf unten. */
.chat-bild:not([style*="aspect-ratio"]) img { height: auto; max-height: 340px; }
.chat-bild:focus-visible { outline: 2px solid var(--akzent); outline-offset: 2px; }
.chat-bild[data-kaputt="ja"] {
line-height: 1.5; padding: 12px 14px;
font-size: .84rem; color: var(--text-leise);
}
/* ---- Ein PDF als Karte ---------------------------------------------- */
.chat-datei {
display: flex; align-items: center; gap: 11px;
margin: 0 0 6px; padding: 10px 12px;
border: 1px solid rgba(150, 186, 220, .2);
border-radius: 12px;
background: rgba(255, 255, 255, .04);
color: inherit; text-decoration: none;
transition: border-color var(--tempo), background var(--tempo);
}
.chat-datei:hover {
border-color: rgba(150, 186, 220, .38);
background: rgba(255, 255, 255, .07);
}
.chat-datei:focus-visible { outline: 2px solid var(--akzent); outline-offset: 2px; }
.chat-datei__zeichen {
flex: none;
width: 38px; height: 38px; border-radius: 10px;
display: grid; place-items: center;
font-size: .72rem; font-weight: 800; letter-spacing: .04em;
color: #ff9aa2;
background: rgba(255, 154, 162, .12);
border: 1px solid rgba(255, 154, 162, .3);
}
.chat-datei__mitte { min-width: 0; display: grid; }
.chat-datei__name {
font-size: .86rem; font-weight: 600; color: var(--text);
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.chat-datei__mass { font-size: .74rem; color: var(--text-leise); }
/* ---- Die Ablagefläche beim Hineinziehen ------------------------------ */
.chat__ablage {
position: absolute; inset: 8px;
z-index: 6;
display: grid; place-items: center;
border: 2px dashed color-mix(in srgb, var(--akzent) 55%, transparent);
border-radius: 16px;
background: rgba(8, 12, 20, .82);
color: var(--text);
font-size: .95rem; font-weight: 650;
pointer-events: none; /* sonst frisst sie das drop-Ereignis */
}
/* ---- Zwischenstand statt Störung ------------------------------------ */
/* "wird geschickt …" ist keine Fehlermeldung. Rot ist hier das
Einzige, was Aufmerksamkeit verlangen darf -- wer es für
Zwischenstände mitbenutzt, macht es wertlos. */
.fehler[data-art="lauf"] { color: var(--text-leise); }
@media (prefers-reduced-motion: reduce) {
.chat__weg, .chat__anhang, .chat-datei { transition: none; }
}
@media (forced-colors: active) {
.chat-datei, .chat__ablage { border: 1px solid CanvasText; }
}