feat: der Dienst nennt sich selbst -- und Cloudflare bricht die WebP-Auslieferung

DER NAME KAM VIERMAL FEST AUS DEM QUELLTEXT: Kopfzeile, Anmeldeseite, Fusszeile
einer Freigabe, Tab-Titel -- und zwar als "d4rk_media", was der Name des
Repositorys ist und nicht der des Dienstes. Wer ihn aendern wollte, musste vier
Stellen finden und eine davon uebersehen.

Er kommt jetzt vom Server, aus der Einstellung marke.name -- DERSELBEN, die
schon in der Fusszeile der Discord-Meldungen steht ("D4RKST3R // MEDIA"). Ein
zweiter Name fuer die Oberflaeche waere ein zweiter, der beim naechsten Mal
anders lautet als der erste.

Ausgeliefert wird er ueber /api/dash/auth/config (oeffentlich, die
Anmeldeseite hat noch keine Sitzung) und in der Freigabe-Antwort (die Seite ist
oeffentlich und holt sonst nichts, was ihn kennen koennte). Angezeigt wird
"<marke> Media" -- die Marke allein waere falsch, sie steht ueber allem, was
hier laeuft.

EINE STELLE BLEIBT ABSICHTLICH: der Schluessel im Speicher des Browsers
(merker.ts) heisst weiter "d4rk_media.". Wer ihn umbenennt, wirft still alle
gemerkten Ansichtseinstellungen weg, und niemand versteht, warum die Galerie
ploetzlich wieder bei 60 Kacheln steht. Steht als Kommentar daneben.

---

UND DER NEUE NAME HAENGT HINTER CLOUDFLARE, was die WebP-Auslieferung bricht.

NPM ist sauber, sogar besser als vorgeschlagen: beide Namen auf DEMSELBEN Host,
die drei ^~-Bloecke gelten damit fuer beide. Der DNS-Eintrag geht aber ueber
Cloudflare (188.114.97.3, Server: cloudflare), waehrend fivemanage direkt auf
88.218.224.10 zeigt.

Gemessen, dieselbe Datei:

    fivemanage (direkt)      ohne Accept  ->  image/png   12295 B   richtig
                              mit Accept  ->  image/webp   2270 B   richtig

    media (Cloudflare)     1. mit Accept  ->  image/webp   2270 B   MISS
                           2. mit Accept  ->  image/webp   2270 B   HIT
                           3. OHNE Accept ->  image/webp   2270 B   HIT   FALSCH

Und andersherum genauso -- wer zuerst kommt, praegt den Zwischenspeicher fuer
alle. Es sind unsere eigenen Bytes (2270 = unsere WebP-Fassung), also kein
Polish, sondern ein Cache-Schluessel ohne Accept. Vary: Accept wird an den
Client durchgereicht, aber nicht zum Unterscheiden benutzt.

Fuer ein Lua-Skript heisst das: items/foo.png liefert WebP-Bytes unter einem
.png-Namen, und das faellt erst auf, wenn irgendwo ein Bild nicht laedt.

PUBLIC_URL ist deshalb NICHT umgestellt. Genau davor stand die Warnung im
letzten Commit, und sie war keine Vorsicht, sondern eine Vorhersage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-12 00:43:29 +02:00
co-authored by Claude Opus 5
parent cf2e3d2387
commit 72fd330ae6
11 changed files with 131 additions and 8 deletions
+36
View File
@@ -960,6 +960,42 @@ einfügen.** Ein neuer Host bringt seine eigene `assets.conf` mit.
**3. Sagen.** Dann setze ich `PUBLIC_URL` um und ziehe Dokumentation, Fotostudio,
Handy und die Watchdog-Einträge nach.
#### ⚠️ DNS und NPM stehen — aber der Name hängt hinter Cloudflare
Am 12.08.2026 eingerichtet. NPM ist dabei sogar besser gelöst als vorgeschlagen:
**beide Namen auf demselben Host** (`server_name fivemanage.d4rkst3r.de
media.d4rkst3r.de;`), die drei `^~`-Blöcke gelten damit für beide.
Der DNS-Eintrag geht aber über Cloudflare, und **das bricht die
WebP-Auslieferung.** Gemessen, dieselbe Datei, dieselbe Sekunde:
```
fivemanage (direkt) ohne Accept -> image/png 12295 B richtig
mit Accept -> image/webp 2270 B richtig
media (Cloudflare) 1. mit Accept -> image/webp 2270 B MISS
2. mit Accept -> image/webp 2270 B HIT
3. OHNE Accept -> image/webp 2270 B HIT ← FALSCH
```
Und andersherum genauso: wer zuerst kommt, prägt den Zwischenspeicher für alle.
Es sind **unsere eigenen Bytes** (2270 B = unsere WebP-Fassung), also kein
Cloudflare-Polish, sondern ein **Cache-Schlüssel ohne `Accept`**. `Vary: Accept`
wird an den Client durchgereicht, aber nicht zum Unterscheiden benutzt —
Cloudflare variiert standardmäßig nur über `Accept-Encoding`.
Für ein Lua-Skript heißt das: `items/foo.png` liefert WebP-Bytes unter einem
`.png`-Namen, und das fällt erst auf, wenn irgendwo ein Bild nicht lädt.
**Deshalb ist `PUBLIC_URL` noch nicht umgestellt.** Zwei Wege heraus:
1. **Graue Wolke** für `media` — Cloudflare-Proxy aus, DNS-only, wie bei
`fivemanage`. Der Ursprung ist ohnehin nicht verborgen (`fivemanage` zeigt
direkt darauf), Cloudflare bringt hier also nichts, was den Bruch aufwöge.
2. Cloudflare behalten und dort eine **Cache Rule** anlegen, die `/f/*` und
`/t/*` vom Zwischenspeicher ausnimmt. Funktioniert, ist aber eine dritte
Stelle, an der dieselbe Regel gepflegt werden will.
Gegenprüfen lässt sich beides mit dem, was schon da ist:
```powershell
+9 -1
View File
@@ -39,6 +39,7 @@ import { mkdir, readdir, readFile, rm, rmdir, stat } from 'node:fs/promises'
import { join } from 'node:path'
import {
DISCORD_KEYS,
MARKE_KEY,
SICHERUNG_KEYS,
ausDatenbank,
discordConfig,
@@ -265,7 +266,14 @@ dashRoutes.get('/auth/me', (c) => {
* Nur ein Ja/Nein: ob der Discord-Knopf ueberhaupt sinnvoll ist. Weder
* Client-ID noch Server-ID gehen hier heraus — die braucht das Formular
* nicht, und was nicht herausgeht, kann auch nicht abgeschrieben werden. */
dashRoutes.get('/auth/config', (c) => c.json({ discord: discordConfig().bereit }))
dashRoutes.get('/auth/config', (c) =>
c.json({
discord: discordConfig().bereit,
// Der Name gehoert MIT hier hinein, obwohl es eine Anmeldeauskunft ist:
// die Anmeldeseite braucht ihn, und die hat noch keine Sitzung. Ohne
// das muesste sie ihn erraten oder fest eingebaut mitbringen.
marke: getSetting(MARKE_KEY) || 'D4RKST3R',
}))
// Der Discord-Weg selbst. MUSS vor der Wache stehen: wer sich anmelden will,
// ist noch nicht angemeldet.
+4
View File
@@ -18,6 +18,7 @@ import { randomBytes } from 'node:crypto'
import { Hono } from 'hono'
import { db, now, type Freigabe, type Media } from '../db.js'
import { config } from '../config.js'
import { getSetting, MARKE_KEY } from '../settings.js'
import { publicUrlFor } from '../storage.js'
export const freigabeRoutes = new Hono()
@@ -63,6 +64,9 @@ freigabeRoutes.get('/:schluessel', (c) => {
.all(`${maskiert}/%`) as Pick<Media, 'path' | 'size' | 'mime' | 'dauer'>[]
return c.json({
// Auch hier der Name aus derselben Einstellung -- die Freigabeseite ist
// oeffentlich und holt sonst nichts, was ihn kennen koennte.
marke: getSetting(MARKE_KEY) || 'D4RKST3R',
titel: eintrag.titel || eintrag.ordner,
ordner: eintrag.ordner,
// Der Praefix, den die Oberflaeche abschneidet -- damit dort
+10
View File
@@ -58,6 +58,16 @@ export function ausDatenbank(key: string): boolean {
// Als JSON in EINER Zeile der Einstellungstabelle. Eine eigene Tabelle fuer
// eine Handvoll Zeilen waere eine Wanderung mehr, die niemand braucht.
/** Wie der Dienst sich nennt.
*
* DIESELBE Einstellung, die auch in der Fusszeile der Discord-Meldungen steht
* (`D4RKST3R // MEDIA`). Ein zweiter Name fuer die Oberflaeche waere ein
* zweiter, der beim naechsten Mal anders lautet als der erste -- und dann
* heisst dasselbe Ding an zwei Stellen verschieden.
*
* Der Standard steht in meldung.ts; hier wird er nur weitergereicht. */
export const MARKE_KEY = 'marke.name'
export const VORLAGEN_KEY = 'vorlagen'
export type Vorlage = { name: string; muster: string }
+1 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>d4rk_media</title>
<title>D4RKST3R Media</title>
<meta name="robots" content="noindex, nofollow" />
</head>
<body>
+3 -1
View File
@@ -25,6 +25,7 @@ import { Freigabe } from './pages/Freigabe'
import { Meldungen } from './components/Meldungen'
import { gehZu, usePfad } from './router'
import { Kontomenue } from './components/Kontomenue'
import { dienstName, useMarke } from './marke'
/** Die Breite des Inhalts — an EINER Stelle.
*
@@ -59,6 +60,7 @@ export function App() {
const checking = useStore((s) => s.checking)
const checkSession = useStore((s) => s.checkSession)
const pfad = usePfad()
const marke = useMarke()
useEffect(() => {
void checkSession()
@@ -114,7 +116,7 @@ export function App() {
className="flex items-center gap-2 font-semibold"
>
<HardDrive size={18} className="text-akzent" />
d4rk_media
{dienstName(marke)}
</a>
<nav className="flex flex-1 gap-1">
+1 -1
View File
@@ -405,7 +405,7 @@ export const api = {
post<{ user: User }>('/api/dash/auth/login', { username, password }),
logout: () => post<{ ok: true }>('/api/dash/auth/logout'),
/** Was die ANMELDESEITE wissen darf, bevor jemand angemeldet ist. */
authConfig: () => get<{ discord: boolean }>('/api/dash/auth/config'),
authConfig: () => get<{ discord: boolean; marke: string }>('/api/dash/auth/config'),
me: () => get<{ user: User }>('/api/dash/auth/me'),
changePassword: (current: string, next: string) =>
post<{ ok: true }>('/api/dash/auth/password', { current, next }),
+52
View File
@@ -0,0 +1,52 @@
import { useEffect, useState } from 'react'
import { api } from './api'
/** Wie der Dienst sich nennt — an EINER Stelle.
*
* Vorher stand „d4rk_media" viermal fest im Quelltext: in der Kopfzeile, auf
* der Anmeldeseite, unter einer Freigabe und im Tab-Titel. Das ist der
* Repository-Name, nicht der Name des Dienstes — und wer ihn ändern wollte,
* musste vier Stellen finden und eine davon übersehen.
*
* Er kommt jetzt vom Server, aus derselben Einstellung, die auch in der
* Fußzeile der Discord-Meldungen steht (`D4RKST3R // MEDIA`). Ein zweiter
* Name für die Oberfläche wäre ein zweiter, der beim nächsten Mal anders
* lautet.
*
* DER STANDARD STEHT HIER TROTZDEM: bis die Antwort da ist, muss etwas in der
* Kopfzeile stehen, und ein leerer Platz, der eine Zehntelsekunde später
* zuspringt, sieht aus wie ein Fehler. */
export const MARKE_STANDARD = 'D4RKST3R'
/** Aus der Marke den Namen des Dienstes: „D4RKST3R" → „D4RKST3R Media".
*
* Die Marke allein wäre falsch — sie steht über allem, was hier läuft
* (Bot, Hub, Gitea). Dieser Dienst ist ihr Medienteil, und genau so heißt er
* auch in der Discord-Fußzeile. */
export const dienstName = (marke: string) => `${marke} Media`
/** Der Name für die angemeldete Oberfläche. Holt ihn einmal und merkt ihn sich
* für die Lebensdauer der Seite — er ändert sich nicht zwischen zwei Klicks. */
export function useMarke(): string {
const [marke, setMarke] = useState(MARKE_STANDARD)
useEffect(() => {
let lebt = true
api.authConfig()
.then((c) => lebt && c.marke && setMarke(c.marke))
.catch(() => {
/* dann eben der Standard — dafür ist er da */
})
return () => {
lebt = false
}
}, [])
// Der Tab-Titel steht in index.html fest und kann die Einstellung nicht
// kennen. Hier nachzuziehen ist die einzige Stelle, an der das geht.
useEffect(() => {
document.title = dienstName(marke)
}, [marke])
return marke
}
+5
View File
@@ -15,6 +15,11 @@ import { useEffect, useState } from 'react'
* der Speicher voll ist. Dann verhält sich der Haken wie ein gewöhnlicher
* Zustand, statt die Seite mitzureißen. */
export function useMerker<T>(schluessel: string, standard: T) {
// ABSICHTLICH der alte, feste Name und NICHT die Marke: das hier ist der
// Schluessel im Speicher des Browsers. Wer ihn umbenennt, wirft still alle
// gemerkten Ansichtseinstellungen weg -- Seitengroesse, Sortierung -- und
// niemand versteht, warum die Galerie ploetzlich wieder bei 60 Kacheln
// steht.
const voll = `d4rk_media.${schluessel}`
const [wert, setWert] = useState<T>(() => {
+7 -2
View File
@@ -3,6 +3,7 @@ import { HardDrive } from 'lucide-react'
import { api } from '../api'
import { useStore } from '../store'
import { Feld, Knopf } from '../components/Bausteine'
import { dienstName, MARKE_STANDARD } from '../marke'
export function Anmelden() {
const login = useStore((s) => s.login)
@@ -12,13 +13,17 @@ export function Anmelden() {
const [wartet, setWartet] = useState(false)
const [fehler, setFehler] = useState<string | null>(null)
const [discord, setDiscord] = useState(false)
const [marke, setMarke] = useState(MARKE_STANDARD)
// Ob der Discord-Knopf ueberhaupt Sinn hat, weiss nur der Dienst. Ein
// Knopf, der zu einer Fehlerseite fuehrt, weil nichts eingerichtet ist,
// ist genau die Sorte Knopf, die wir hier nicht bauen.
useEffect(() => {
api.authConfig()
.then((c) => setDiscord(c.discord))
.then((c) => {
setDiscord(c.discord)
if (c.marke) setMarke(c.marke)
})
.catch(() => setDiscord(false))
}, [])
@@ -75,7 +80,7 @@ export function Anmelden() {
<span className="flex h-9 w-9 items-center justify-center rounded-lg bg-akzent/15 text-akzent">
<HardDrive size={20} />
</span>
<h1 className="text-lg font-semibold">d4rk_media</h1>
<h1 className="text-lg font-semibold">{dienstName(marke)}</h1>
</div>
<p className="mb-6 text-xs text-schrift-leise">
Bilder, Videos und Ton für die eigenen Ressourcen.
+3 -2
View File
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react'
import { FileQuestion, Film, Image, Loader2, Music, Play } from 'lucide-react'
import { bytes, dauerText, istBild } from '../format'
import { dienstName, MARKE_STANDARD } from '../marke'
/** Ein freigegebener Ordner ohne Anmeldung.
*
@@ -21,7 +22,7 @@ type Eintrag = {
thumb: string
}
type Seite = { titel: string; ordner: string; anzahl: number; items: Eintrag[] }
type Seite = { marke: string; titel: string; ordner: string; anzahl: number; items: Eintrag[] }
export function Freigabe({ schluessel }: { schluessel: string }) {
const [daten, setDaten] = useState<Seite | null>(null)
@@ -107,7 +108,7 @@ export function Freigabe({ schluessel }: { schluessel: string }) {
</div>
<p className="mt-6 text-center text-[11px] text-schrift-leise">
Bereitgestellt mit d4rk_media
Bereitgestellt mit {dienstName(daten.marke || MARKE_STANDARD)}
</p>
</main>