Files
d4rk_media/server/ui/src/App.tsx
T
D4rkst3randClaude Opus 5 72fd330ae6 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>
2026-08-12 00:43:29 +02:00

160 lines
6.3 KiB
TypeScript

import { useEffect } from 'react'
import {
Code2,
Gauge,
HardDrive,
History,
Images,
KeyRound,
PieChart,
Settings,
Trash2,
} from 'lucide-react'
import { useStore } from './store'
import { Anmelden } from './pages/Anmelden'
import { Start } from './pages/Start'
import { Galerie } from './pages/Galerie'
import { Speicher } from './pages/Speicher'
import { Tokens } from './pages/Tokens'
import { Konto } from './pages/Konto'
import { Einstellungen } from './pages/Einstellungen'
import { Verlauf } from './pages/Verlauf'
import { Api } from './pages/Api'
import { Papierkorb } from './pages/Papierkorb'
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.
*
* Sie stand vorher zweimal da (Kopfzeile und Inhalt), und zwei Seiten setzten
* zusätzlich ihre eigene: dadurch waren Konto und Einstellungen schmaler als
* der Rest, und die Kopfzeile lief über beiden anders. Formulare werden
* stattdessen INNERHALB ihrer Karte begrenzt.
*/
const BREITE = 'mx-auto w-full max-w-7xl px-4'
const seiten = [
{ pfad: '/', name: 'Start', kind: Gauge, Inhalt: Start },
{ pfad: '/galerie', name: 'Galerie', kind: Images, Inhalt: Galerie },
{ pfad: '/verlauf', name: 'Verlauf', kind: History, Inhalt: Verlauf },
{ pfad: '/speicher', name: 'Speicher', kind: PieChart, Inhalt: Speicher },
{ pfad: '/papierkorb', name: 'Papierkorb', kind: Trash2, Inhalt: Papierkorb },
{ pfad: '/tokens', name: 'Token', kind: KeyRound, Inhalt: Tokens },
{ pfad: '/api', name: 'API', kind: Code2, Inhalt: Api },
{ pfad: '/einstellungen', name: 'Einstellungen', kind: Settings, Inhalt: Einstellungen },
] as const
/** Seiten OHNE Reiter. Sie sind ueber die Adresse und ueber das Kontomenue
* erreichbar -- ein Lesezeichen auf /konto soll nicht ins Leere fuehren, nur
* weil der Reiter verschwunden ist. */
const versteckt = [{ pfad: '/konto', Inhalt: Konto }] as const
export function App() {
// Einzeln ausgewählt und nicht als ganzer Laden: `useStore()` ohne
// Auswahlfunktion abonniert ALLES, und dann zeichnet jede Meldung das
// ganze Dashboard neu — bei 60 Kacheln mit Bildern ist das spürbar.
const user = useStore((s) => s.user)
const checking = useStore((s) => s.checking)
const checkSession = useStore((s) => s.checkSession)
const pfad = usePfad()
const marke = useMarke()
useEffect(() => {
void checkSession()
}, [checkSession])
// EINE FREIGABE GEHT VOR ALLEM ANDEREN -- vor dem Prüfen der Sitzung, vor
// dem Anmeldeformular, vor der Kopfzeile. Wer einen Link bekommen hat, ist
// kein Benutzer dieses Dashboards; ihm ein Anmeldeformular zu zeigen wäre
// die falsche Antwort auf "sieh dir das an".
//
// Und deshalb steht das hier VOR dem `checking`-Zweig: sonst blitzt für
// einen Moment das Rädchen der Sitzungsprüfung auf, die es gar nicht
// braucht.
if (pfad.startsWith('/s/')) {
return <Freigabe schluessel={decodeURIComponent(pfad.slice(3))} />
}
// Solange nicht feststeht, ob eine Sitzung besteht, wird NICHTS gezeigt.
// Sonst blitzt bei jedem Neuladen kurz das Anmeldeformular auf, obwohl man
// angemeldet ist — und das sieht aus wie ein Fehler.
if (checking) {
return (
<div className="flex min-h-full items-center justify-center">
<HardDrive className="animate-pulse text-schrift-leise" />
<Meldungen />
</div>
)
}
if (!user) {
return (
<>
<Anmelden />
<Meldungen />
</>
)
}
const ohneReiter = versteckt.find((s) => s.pfad === pfad)
const aktiv = seiten.find((s) => s.pfad === pfad) ?? seiten[0]
const Inhalt = ohneReiter ? ohneReiter.Inhalt : aktiv.Inhalt
return (
<div className="flex min-h-full flex-col">
<header className="sticky top-0 z-40 border-b border-rand bg-flaeche/80 backdrop-blur">
<div className={`${BREITE} flex flex-wrap items-center gap-x-6 gap-y-2 py-2.5`}>
<a
href="/"
onClick={(e) => {
e.preventDefault()
gehZu('/')
}}
className="flex items-center gap-2 font-semibold"
>
<HardDrive size={18} className="text-akzent" />
{dienstName(marke)}
</a>
<nav className="flex flex-1 gap-1">
{seiten.map((s) => {
const an = s.pfad === aktiv.pfad
return (
<a
key={s.pfad}
href={s.pfad}
onClick={(e) => {
e.preventDefault()
gehZu(s.pfad)
}}
className={`relative flex items-center gap-1.5 rounded-md px-2.5 py-1.5
text-sm transition-colors ${
an
? 'bg-flaeche-hoch text-schrift after:absolute after:inset-x-2.5 after:-bottom-[9px] after:h-0.5 after:rounded-full after:bg-akzent'
: 'text-schrift-leise hover:bg-flaeche-hoch/40 hover:text-schrift'
}`}
>
<s.kind size={15} />
{s.name}
</a>
)
})}
</nav>
<Kontomenue />
</div>
</header>
<main key={aktiv.pfad} className={`auftauchen ${BREITE} flex-1 py-5`}>
<Inhalt />
</main>
<Meldungen />
</div>
)
}