feat: das Dashboard -- und die eine Regel steht im Code, nicht im Vorsatz

Schritt 4. React 18, TypeScript, Vite 5, Tailwind 4 (CSS-first), Lucide,
Zustand; kein Konsta. Quelle in server/ui, Build nach server/web (gitignored),
im Abbild von einer eigenen Docker-Stufe gebaut.

JEDER KNOPF GIBT RUECKMELDUNG -- und zwar erzwungen, nicht vorgenommen:

- api.ts haelt den EINZIGEN fetch und GENAU EINE Fehlerklasse. Der
  Fivemanage-Fehler war, zwei zu haben: geworfen wurde `new Error`, geprueft
  auf `instanceof ApiError`, und damit verschwand jeder Fehlschlag lautlos.
- store.ts haelt run(): Aktion rein, Erfolgstext rein, und es meldet bei
  Erfolg diesen und bei Fehlschlag den Text DES DIENSTES. Wer darueber geht,
  kann keinen stillen Knopf bauen.
- Jeder wartende Knopf ist gesperrt und zeigt einen Kreisel. Der zweite Klick
  eine Sekunde spaeter war der Weg zu neunzehn gleichnamigen Organisationen.
- Fehlermeldungen bleiben stehen, bis jemand sie wegklickt. Ein Fehler, der
  von allein verschwindet, ist einer, den niemand gelesen hat.

@vitejs/plugin-react ist auf ^4 festgenagelt: die 6 verlangt vite ^8. In den
peerDependencies nachgesehen -- Vite 5.4.21 liegt in der Schnittmenge von
plugin-react@4 und @tailwindcss/vite@4.

Am laufenden Container nachgemessen: / kommt als text/html, assets/*.js als
text/javascript, *.css als text/css (die MIME-Falle aus Schritt 1 traegt), und
/tokens, /speicher, /konto liefern dieselbe index.html -- die SPA-Rueckfall-
route tut, wofuer sie gebaut wurde.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 17:38:38 +02:00
co-authored by Claude Opus 5
parent 65828b526c
commit 6c8ead8e15
22 changed files with 4644 additions and 15 deletions
+84
View File
@@ -0,0 +1,84 @@
import { useState, type FormEvent } from 'react'
import { api } from '../api'
import { run, useStore } from '../store'
import { Feld, Karte, Knopf } from '../components/Bausteine'
export function Konto() {
const user = useStore((s) => s.user)
const note = useStore((s) => s.note)
const [aktuell, setAktuell] = useState('')
const [neu, setNeu] = useState('')
const [wieder, setWieder] = useState('')
const [wartet, setWartet] = useState(false)
const zuKurz = neu.length > 0 && neu.length < 8
const ungleich = wieder.length > 0 && neu !== wieder
async function wechseln(e: FormEvent) {
e.preventDefault()
// Die Wiederholung prüft nur diese Seite — der Dienst kennt sie nicht.
// Deshalb hier ein eigener Satz statt einer Anfrage, die nichts
// bewirken kann.
if (neu !== wieder) {
note('fehler', 'Die beiden neuen Passwörter stimmen nicht überein.')
return
}
setWartet(true)
const ok = await run(
() => api.changePassword(aktuell, neu),
'Passwort geändert. Alle anderen Anmeldungen wurden beendet.',
)
setWartet(false)
if (ok) {
setAktuell('')
setNeu('')
setWieder('')
}
}
return (
<div className="max-w-md">
<Karte titel={`Angemeldet als ${user?.username ?? '—'}`}>
<form onSubmit={wechseln} className="flex flex-col gap-3">
<Feld
label="Aktuelles Passwort"
type="password"
autoComplete="current-password"
value={aktuell}
onChange={(e) => setAktuell(e.target.value)}
/>
<Feld
label="Neues Passwort"
type="password"
autoComplete="new-password"
value={neu}
onChange={(e) => setNeu(e.target.value)}
hinweis={zuKurz ? 'Mindestens 8 Zeichen.' : undefined}
/>
<Feld
label="Neues Passwort wiederholen"
type="password"
autoComplete="new-password"
value={wieder}
onChange={(e) => setWieder(e.target.value)}
hinweis={ungleich ? 'Stimmt nicht mit dem neuen Passwort überein.' : undefined}
/>
<p className="text-xs text-schrift-leise">
Ein Wechsel beendet alle anderen Sitzungen. Diese hier bleibt bestehen.
</p>
<div>
<Knopf
art="haupt"
type="submit"
wartet={wartet}
disabled={!aktuell || neu.length < 8 || neu !== wieder}
kind="Passwort ändern"
/>
</div>
</form>
</Karte>
</div>
)
}