feat(frontend): v3-Umbau P2 — echtes Routing, Unterzustand in der URL
Dritte Etappe. Die Ansicht lag bisher im URL-Hash (`#models`), gespeist aus einem
useState in App.tsx. Das war fuer Reload und Lesezeichen brauchbar — aber nur auf
View-Ebene; fuer den Zustand DARUNTER war kein Platz.
## Was jetzt in der Adresse steht (Befund B-05)
/modelle?reiter=routing der Reiter war lokaler State, nicht teilbar
/ideen?offen=<id> die aufgeklappte Karte; wer waehrend eines
laufenden Workers neu lud, verlor seinen Platz
/wissen?datei=<pfad> jede Vault-Notiz verlinkbar
<ueberall>?system=wartung|logs die System-Schublade inkl. Reiter
Alle Suchparameter werden validiert (app/router.tsx) — ein Link mit Unsinn darin
faellt auf den Normalfall zurueck, statt die Ansicht in einen halben Zustand zu
bringen.
## Alte Lesezeichen brechen nicht
lib/hashUmleitung.ts biegt `#models` per replaceState auf `/modelle`, EINMAL vor
dem Router-Mount. Fuenf Tests decken das ab (jeder NAV-Eintrag, die Schreibweise
`#/models`, erhaltene Suchparameter, unbekannter Hash, kein Hash) — die Bruecke
sieht sonst wie toter Code aus und waere ein naheliegender Kandidat zum Wegraeumen.
Das Backend brauchte KEINE Zeile: der SPA-Catch-all in app.py:148-168 beantwortet
jede unbekannte Route schon immer mit index.html. Vorher geprueft, nicht gehofft.
## Fehlergrenze pro Route (Befund B-13)
Vorher hing eine einzige Grenze in main.tsx um alles — ein Renderfehler im
Modell-Manager nahm Cockpit, Konsole und Chronik mit. Jetzt faengt jede Route fuer
sich (components/RouteFehler.tsx: was passiert ist, was man tun kann, technische
Einzelheiten aufklappbar). AppErrorBoundary bleibt als letztes Netz fuer Fehler
ausserhalb jeder Route. Dazu eine 404-Seite, die Schiene und Kopfzeile stehen
laesst — im Browser geprueft.
## Budget: der Router kostete 29 kB, sie sind wieder drin
TanStack Router hob den Start-Chunk von 111 auf 141 kB gzip und riss damit das in
P0 gesetzte Ampel-Budget (125 kB). Statt das Budget hochzusetzen — was es als
Signal entwertet haette — den Platz zurueckgeholt: SystemDrawer und CommandPalette
sind beim Start UNSICHTBAR und liegen jetzt hinter lazy(). Die Palette laedt beim
ersten Strg+K und bleibt dann gemountet.
Start-Chunk gzip 111 411 -> 114 978 B (Budget 125 000, weiter scharf)
dist gesamt 1 565 521 B (Budget 3 145 728)
## Nebenbei
· Befund B-09 erledigt: Der "Suchen"-Knopf baute ein GEFAELSCHTES KeyboardEvent
(`new KeyboardEvent("keydown", { metaKey: true })`) und feuerte es aufs
Dokument. Jetzt ein eigener Kanal; in P3 loest der Store auch den ab.
· Der Schliessen-Knopf der System-Schublade hatte kein aria-label — ein reiner
Icon-Knopf ohne Namen. Nachgeholt (beim Pruefen aufgefallen).
· nav.ts fuehrt jetzt den Pfad je Eintrag + navFuerPfad() mit Laengen-Sortierung,
damit /ideen/<id> die Sidebar bei "Ideen" markiert. Mit Tests.
## Verifiziert im Browser (lokales Backend, echtes Produktions-Buendel)
/ -> /cockpit, Sidebar markiert
/#models -> /modelle, Sidebar markiert
/modelle?reiter=routing -> Reiter "Routing & Warm-Set" aktiv
/cockpit?system=logs -> Schublade offen, Reiter "System-Logs" aktiv;
Schliessen entfernt den Parameter wieder
/gibtsnicht -> 404-Seite, Schiene + Kopfzeile intakt
Zurueck-Taste -> voriger Pfad
26/26 Tests gruen · ESLint 0 Fehler · tsc sauber.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ae69c5ce31
commit
464b0d20b8
@@ -1,9 +1,11 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { Command } from "cmdk"
|
||||
import { NAV, type ViewId } from "@/nav"
|
||||
import { useNavigate } from "@tanstack/react-router"
|
||||
import { NAV } from "@/nav"
|
||||
|
||||
export function CommandPalette({ onNavigate }: { onNavigate: (v: ViewId) => void }) {
|
||||
export function CommandPalette() {
|
||||
const [open, setOpen] = useState(false)
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
@@ -16,6 +18,16 @@ export function CommandPalette({ onNavigate }: { onNavigate: (v: ViewId) => void
|
||||
return () => document.removeEventListener("keydown", onKey)
|
||||
}, [])
|
||||
|
||||
// Der „Suchen"-Knopf in der Kopfzeile (v3-Umbau P2, Befund B-09).
|
||||
// Er baute vorher ein GEFÄLSCHTES Tastatur-Ereignis (`new KeyboardEvent("keydown",
|
||||
// { metaKey: true })`) und feuerte es aufs Dokument, damit der Zuhörer oben anspringt.
|
||||
// Jetzt ein eigener, ehrlicher Kanal. In P3 löst der Store auch den ab.
|
||||
useEffect(() => {
|
||||
const onOpen = () => setOpen(true)
|
||||
window.addEventListener("mc-palette-open", onOpen)
|
||||
return () => window.removeEventListener("mc-palette-open", onOpen)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Command.Dialog
|
||||
open={open}
|
||||
@@ -43,7 +55,7 @@ export function CommandPalette({ onNavigate }: { onNavigate: (v: ViewId) => void
|
||||
key={item.id}
|
||||
value={`${item.label} ${item.hint}`}
|
||||
onSelect={() => {
|
||||
onNavigate(item.id)
|
||||
navigate({ to: item.pfad })
|
||||
setOpen(false)
|
||||
}}
|
||||
className="flex cursor-pointer items-center gap-3 rounded-md px-3 py-2 text-sm text-foreground aria-selected:bg-accent aria-selected:text-accent-foreground"
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { AlertTriangle, RotateCcw } from "lucide-react"
|
||||
|
||||
// Fehlerfläche EINER Ansicht (v3-Umbau P2, Befund B-13).
|
||||
//
|
||||
// Vorher hing genau eine Fehlergrenze in main.tsx um die gesamte App: Ein Renderfehler
|
||||
// im Modell-Manager nahm Cockpit, Konsole und Chronik gleich mit — die Seite war weiß.
|
||||
// Jetzt fängt jede Route für sich; Schiene, Kopfzeile und Statusanzeige bleiben stehen,
|
||||
// und der Nutzer kann einfach woanders hin.
|
||||
//
|
||||
// Die Meldung sagt, was passiert ist und was man tun kann. Der technische Text steht
|
||||
// aufklappbar darunter — nicht versteckt (man braucht ihn für einen Fehlerbericht),
|
||||
// aber auch nicht als Erstes im Gesicht.
|
||||
export function RouteFehler({ titel, text, fehler, onErneut }: {
|
||||
titel: string
|
||||
text: string
|
||||
fehler?: unknown
|
||||
onErneut?: () => void
|
||||
}) {
|
||||
const detail = fehler instanceof Error ? (fehler.stack || fehler.message) : fehler ? String(fehler) : null
|
||||
|
||||
return (
|
||||
<div className="flex min-h-[50vh] items-center justify-center p-6">
|
||||
<div className="mc-card max-w-lg space-y-4 p-6">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<AlertTriangle className="h-5 w-5 shrink-0 text-amber-400" aria-hidden="true" />
|
||||
<h1 className="font-space text-lg font-bold tracking-tight">{titel}</h1>
|
||||
</div>
|
||||
|
||||
<p className="text-sm leading-relaxed text-muted-foreground">{text}</p>
|
||||
|
||||
<div className="flex flex-wrap gap-2 pt-1">
|
||||
{onErneut && (
|
||||
<button
|
||||
onClick={onErneut}
|
||||
className="flex h-9 cursor-pointer items-center gap-1.5 rounded-lg bg-primary px-4 text-xs font-semibold text-primary-foreground transition-colors hover:bg-primary/90"
|
||||
>
|
||||
<RotateCcw className="h-3.5 w-3.5" aria-hidden="true" /> Erneut versuchen
|
||||
</button>
|
||||
)}
|
||||
<a
|
||||
href="/cockpit"
|
||||
className="flex h-9 items-center rounded-lg border border-border/60 bg-background/20 px-4 text-xs font-semibold text-muted-foreground transition-colors hover:bg-accent"
|
||||
>
|
||||
Zum Cockpit
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{detail && (
|
||||
<details className="pt-1">
|
||||
<summary className="cursor-pointer text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70 hover:text-foreground">
|
||||
Technische Einzelheiten
|
||||
</summary>
|
||||
<pre className="scrollbar-thin mt-2 max-h-52 overflow-auto rounded-lg border border-border/40 bg-background/40 p-3 font-mono text-[10px] leading-relaxed text-muted-foreground">
|
||||
{detail}
|
||||
</pre>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -311,9 +311,10 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex h-8 w-8 items-center justify-center rounded-lg border border-border/40 text-muted-foreground hover:bg-accent transition-colors"
|
||||
aria-label="System-Schublade schließen"
|
||||
className="flex h-8 w-8 items-center justify-center rounded-lg border border-border/40 text-muted-foreground hover:bg-accent transition-colors cursor-pointer"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
<X className="h-4 w-4" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user