feat: Betrachter fuer Lesbares, und aus dem <video> wird ein Player

Bild, Video, Ton und PDF konnte der Betrachter schon. Was fehlte, war
ausgerechnet die Art, die man am ehesten nur lesen und nicht herunterladen
will: eine .md landete im Zweig "laesst sich hier nicht anzeigen".

LESBARES. Markdown gesetzt, .lua und .json eingefaerbt, CSV als Tabelle, alles
Uebrige mit Zeilennummern. Der Tokenizer ist DERSELBE wie fuer die Schnipsel
auf der API-Seite und nicht ein zweiter -- zwei waeren zwei, die auseinander
laufen. Die Zeilennummern liegen in einer eigenen Spalte: so nimmt ein
Markieren mit der Maus sie nicht mit.

Die CSV-Zerlegung kann Anfuehrungszeichen und doppelte darin. Das ist der Teil,
den ein split(';') falsch macht, und CSV aus Excel hat ihn regelmaessig; das
Trennzeichen wird aus der Kopfzeile geraten.

NICHT ALLES WIRD GEHOLT. Die Groesse steht in der Datenbank, also wissen wir
vorher, worauf wir uns einlassen: ueber 512 KB kommt per Bereichsanfrage nur
der Anfang, und das steht auch da. Ein Betrachter, der bei einem 400-MB-
Protokoll den Tab abschiesst, ist schlimmer als einer, der die Datei gar nicht
erst oeffnet. Gemessen: Range: bytes=0-49 -> 206, 50 Bytes.

MARKDOWN OHNE BIBLIOTHEK, und das ist kein Geiz. Der uebliche Weg heisst marked
plus DOMPurify und endet bei dangerouslySetInnerHTML -- damit haengt die
Sicherheit dieser Seite an der Frage, ob die Filterliste vollstaendig ist. Hier
entsteht NIE eine HTML-Zeichenkette: der Text wird zu React-Knoten, und ein
<script> in einer hochgeladenen .md erscheint als die acht Zeichen, die es ist.
Im ganzen ui/ steht kein einziges dangerouslySetInnerHTML.

Die eine Luecke, die React nicht schliesst, ist href: [klick](javascript:...)
kaeme durch. Zwoelf Faelle in Node durchgeprueft, alle bestanden -- http,
https, mailto und relative Ziele durch; javascript:, JavaScript:, mit
Leerzeichen davor, data:, vbscript:, file: abgelehnt und als Text stehen
gelassen.

DER PLAYER. Die Bedienelemente bleiben die des Browsers: ein selbstgebauter
Schieber sieht in jedem Browser anders falsch aus, kennt keine
Tastatursteuerung und keine Untertitel. Drumherum kam, was der Browser nicht
mitbringt.

Lautstaerke ueber Dateien hinweg -- <video> setzt sie bei jedem neuen Element
auf 1 zurueck, und wer sich durch zwanzig Clips klickt, stellt sie sonst
zwanzigmal leise, waehrend der einundzwanzigste ungefragt wieder hochfaehrt.

Weiter zum naechsten Stueck, abschaltbar, und nur wenn das Naechste auch etwas
zum Abspielen ist: nach einem Lied ungefragt bei einem Bild zu landen waere
schlechter als stehenzubleiben.

Tastatur an EINER Stelle: Leertaste haelt an, Pfeile springen fuenf Sekunden,
Umschalt+Pfeil wechselt die Datei. Bei allem anderen blaettern die Pfeile wie
bisher. Dass das so ist, steht als Zeile unter dem Bild -- eine unsichtbare
Sonderregel fuehlt sich an wie ein Fehler.

SERVER. Die Typtabelle kennt jetzt md, markdown, csv, lua, xml, yml, yaml,
toml, ini, cfg, sql und log -- alle als text/plain. Nicht als text/markdown
(braechte nichts) und erst recht nicht als text/html: wer einen Upload-Token
hat, koennte damit eine Seite unter unserem Namen veroeffentlichen. html, css
und js stehen deshalb weiterhin NICHT drin.

Der Betrachter entscheidet trotzdem nach der Endung und erst danach nach der
gemeldeten Art. Grund steht in der Datenbank: items/readme.md lag schon da und
trug application/octet-stream, weil sie vor der erweiterten Tabelle hochgeladen
wurde. Wer nur auf die Art schaut, zeigt die alte README nicht an und eine neue
schon -- und sucht den Unterschied an der falschen Stelle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 21:58:10 +02:00
co-authored by Claude Opus 5
parent aa820c32af
commit 7b35ef5698
9 changed files with 862 additions and 30 deletions
+22
View File
@@ -81,6 +81,28 @@ const MIME: Record<string, string> = {
pdf: 'application/pdf',
json: 'application/json',
txt: 'text/plain; charset=utf-8',
// Lesbares. ALLES ALS text/plain, auch das Markdown -- und das ist die
// Stelle, an der man sich vertun kann: eine eigene Art wie text/markdown
// brächte nichts, aber text/html brächte eine Luecke. Wer einen
// Upload-Token hat, koennte damit eine Seite unter unserem Namen
// veroeffentlichen; als text/plain zeigt der Browser den Quelltext an und
// fuehrt nichts aus.
//
// Deshalb stehen html, css und js hier NICHT, und das soll auch so
// bleiben. Die Oberflaeche hat dafuer ihre eigene Tabelle in index.ts.
md: 'text/plain; charset=utf-8',
markdown: 'text/plain; charset=utf-8',
csv: 'text/plain; charset=utf-8',
lua: 'text/plain; charset=utf-8',
xml: 'text/plain; charset=utf-8',
yml: 'text/plain; charset=utf-8',
yaml: 'text/plain; charset=utf-8',
toml: 'text/plain; charset=utf-8',
ini: 'text/plain; charset=utf-8',
cfg: 'text/plain; charset=utf-8',
sql: 'text/plain; charset=utf-8',
log: 'text/plain; charset=utf-8',
}
export function mimeFor(path: string): string {