import { useState } from "react"; import { NavLink, Navigate, Route, Routes, useLocation } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; import { useAuth } from "./auth"; import { api, type SystemStatus } from "./api"; import { LoginPage } from "./pages/Login"; import { ClientsPage } from "./pages/Clients"; import { RolesPage } from "./pages/Roles"; import { GroupsPage } from "./pages/Groups"; import { BrokerConfigPage } from "./pages/BrokerConfig"; import { CertificatesPage } from "./pages/Certificates"; import { SystemPage } from "./pages/System"; import { StatusPage } from "./pages/Status"; import { TopicExplorerPage } from "./pages/TopicExplorer"; // Navigation structure per spec section 57. Entries with a `to` route are // implemented; entries without one are planned pages, rendered greyed out // so the full target information architecture stays visible. const NAV_SECTIONS: { title: string; items: { label: string; to?: string; hint?: string }[]; }[] = [ { title: "Broker", items: [ { label: "Status", to: "/broker/status" }, { label: "Configuration", to: "/broker" }, { label: "Listeners", to: "/broker#listeners" }, { label: "Bridges", hint: "Bridge management (post-MVP)" }, { label: "Revisions", to: "/broker#revisions" }, ], }, { title: "MQTT Security", items: [ { label: "Clients", to: "/clients" }, { label: "Groups", to: "/groups" }, { label: "Roles", to: "/roles" }, { label: "ACL Simulator", hint: "ACL simulator (post-MVP)" }, { label: "Authentication", hint: "Auth mechanism configuration (Release 2)" }, ], }, { title: "Certificates", items: [ { label: "Authorities", to: "/certificates" }, { label: "Server Certificates", to: "/certificates" }, { label: "Client Certificates", to: "/certificates#client-certificates" }, { label: "CSRs", hint: "CSR signing (Release 2, later)" }, { label: "Revocations", hint: "CRL management (Release 2, later)" }, ], }, { title: "Tools", items: [ { label: "Topic Explorer", to: "/topics" }, { label: "Connection Test", hint: "Connectivity tester (post-MVP)" }, { label: "Client Provisioning", hint: "Provisioning wizard (post-MVP)" }, ], }, { title: "Administration", items: [ { label: "Users", hint: "User management page (Release 3)" }, { label: "Roles", hint: "Management roles admin (Release 3)" }, { label: "Audit Log", hint: "Audit viewer (Release 3)" }, { label: "Backup", hint: "Backup/restore (Release 3)" }, { label: "System", to: "/system" }, ], }, ]; function RestartIndicator() { const { data } = useQuery({ queryKey: ["system-status"], queryFn: () => api.get("/system/status"), refetchInterval: 15000, }); if (!data?.restart_required) return null; return ( ⚠ Restart needed — listener changes are pending ); } // Hash-aware active matching: an item is active only when BOTH its path // AND its hash (if any) match the current location. NavLink's default // prefix matching lights up every /broker* item on /broker/status, and // hash items (#listeners, #revisions) all match on pathname alone. function isItemActive( to: string, location: { pathname: string; hash: string }, ): boolean { const [path, hash] = to.split("#"); if (path !== location.pathname) return false; return hash ? location.hash === `#${hash}` : location.hash === ""; } function Layout({ children }: { children: React.ReactNode }) { const { session, logout } = useAuth(); const location = useLocation(); // Exactly ONE group is open at a time (null = all closed). The group // containing the ACTIVE page auto-opens, so navigation (including the // restart-needed indicator link) always reveals its section. const [openSection, setOpenSection] = useState(null); const activeSection = NAV_SECTIONS.find((section) => section.items.some( (item) => item.to && isItemActive(item.to, location), ), ); return (
{children}
); } function Protected({ children }: { children: React.ReactNode }) { const { session, loading } = useAuth(); const location = useLocation(); if (loading) return

Loading…

; if (!session) { // Remember the guarded route so Login can return here after sign-in. return ; } return {children}; } export function App() { return ( } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> ); }