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>
56 lines
2.9 KiB
TypeScript
56 lines
2.9 KiB
TypeScript
import {
|
|
LayoutDashboard,
|
|
Boxes,
|
|
Inbox,
|
|
History,
|
|
Library,
|
|
Plug,
|
|
Bot,
|
|
SquareTerminal,
|
|
HelpCircle,
|
|
Lightbulb,
|
|
type LucideIcon,
|
|
} from "lucide-react"
|
|
|
|
export type ViewId = "dashboard" | "ideen" | "auftraege" | "skills" | "models" | "wissen" | "chronik" | "connect" | "agent" | "konsole" | "guide"
|
|
|
|
export type NavGroup = "Operativ" | "Wissen" | "Werkzeuge" | "Wartung"
|
|
|
|
export interface NavItem {
|
|
id: ViewId
|
|
/** Echter URL-Pfad (v3-Umbau P2). Vorher war die Ansicht nur ein Hash (#models) —
|
|
* teilbar auf View-Ebene, aber ohne Platz für den Zustand DARUNTER. */
|
|
pfad: string
|
|
label: string
|
|
hint: string
|
|
icon: LucideIcon
|
|
group: NavGroup
|
|
}
|
|
|
|
// Eine Quelle der Wahrheit für Sidebar UND Command-Palette.
|
|
export const NAV: NavItem[] = [
|
|
{ id: "dashboard", pfad: "/cockpit", label: "Cockpit", hint: "Deine Box auf einen Blick", icon: LayoutDashboard, group: "Operativ" },
|
|
// Der Haupteingang: Idee → Konzept → Repo → IDE. Steht bewusst direkt unter dem
|
|
// Cockpit — hier fängt jede Arbeit an (eigene Seite seit 25.07.2026).
|
|
// Hinweis bewusst kurz: die App zeigt ihn oben in Großbuchstaben, direkt über der
|
|
// Seitenüberschrift „Ideen" — ein langer Satz stand dort dreifach gedoppelt.
|
|
{ id: "ideen", pfad: "/ideen", label: "Ideen", hint: "Schreib hin, was entstehen soll", icon: Lightbulb, group: "Operativ" },
|
|
{ id: "auftraege", pfad: "/auftraege", label: "Auftragsbuch", hint: "Vorschläge der Box — annehmen oder ablehnen", icon: Inbox, group: "Operativ" },
|
|
{ id: "skills", pfad: "/skills", label: "Skills & Jobs", hint: "Autonome Skills manuell auslösen", icon: Bot, group: "Operativ" },
|
|
{ id: "chronik", pfad: "/chronik", label: "Chronik", hint: "Was die Box von allein getan hat + Zeitmaschine", icon: History, group: "Operativ" },
|
|
{ id: "wissen", pfad: "/wissen", label: "Wissen", hint: "Lucys Wissens-Vault (Traum-Notizen)", icon: Library, group: "Wissen" },
|
|
{ id: "connect", pfad: "/verbinden", label: "Verbinden", hint: "IDE-/Agent-Configs erzeugen", icon: Plug, group: "Werkzeuge" },
|
|
{ id: "konsole", pfad: "/konsole", label: "Konsole", hint: "Direkte Box-Shell (SSH-artig)", icon: SquareTerminal, group: "Werkzeuge" },
|
|
{ id: "guide", pfad: "/anleitung", label: "Anleitung", hint: "Einrichten & Vibe-Coding", icon: HelpCircle, group: "Werkzeuge" },
|
|
{ id: "models", pfad: "/modelle", label: "Modelle", hint: "Speicher, laden & Rollen", icon: Boxes, group: "Wartung" },
|
|
{ id: "agent", pfad: "/agent", label: "Hermes", hint: "Agent-Status & Verdrahtung", icon: Bot, group: "Wartung" },
|
|
]
|
|
|
|
/** Nav-Eintrag zu einem Pfad — für Kopfzeile und aktive Markierung.
|
|
* Längster Treffer gewinnt, damit /ideen/<id> beim Eintrag „Ideen" landet. */
|
|
export function navFuerPfad(pfad: string): NavItem | undefined {
|
|
return [...NAV]
|
|
.sort((a, b) => b.pfad.length - a.pfad.length)
|
|
.find((n) => pfad === n.pfad || pfad.startsWith(n.pfad + "/"))
|
|
}
|