Split the web UI into a page per section

The dashboard was a single route that swapped section components via a
`section` state field. Convert it to the App Router's multi-page model so
each left-nav selection is its own route (/, /repositories, /agents,
/schedules, /approvals, /servers, /activity, /shared, /login), making the
pages modular and independently updatable.

- Move the token gate + store provider + sidebar into a persistent frame
  (AppFrame + Shell) rendered by the root layout, so auth, the polling
  loop, and shared state survive client-side navigation.
- Sidebar items are now <Link> routes; the active item and the store's
  polled section are derived from the URL (lib/nav).
- Each section gets an app/<section>/page.tsx; Runs stays at root and keeps
  its full-height split layout, the rest render in the shared scroll frame.
- Emit per-route index.html (trailingSlash) so the FastAPI StaticFiles
  mount serves clean slash-terminated URLs with no SPA rewrite.
- Regenerate the bundled static export.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsAeGVadULRzPhV2PRDttM
This commit is contained in:
Claude
2026-07-22 19:34:33 +00:00
parent c600ad481e
commit 5d8c62450c
58 changed files with 410 additions and 174 deletions
+57
View File
@@ -0,0 +1,57 @@
/* Auth frame: token gate → shell. Lives in the root layout so it wraps every page and
* persists across client-side navigation. Client-only; the exported HTML is a shell and
* every byte of data is fetched by the browser from the authed API after the token is
* supplied. A 401 from any call clears the token and re-prompts with an error. */
"use client";
import { useCallback, useEffect, useState } from "react";
import { usePathname } from "next/navigation";
import { DashboardProvider } from "@/components/store";
import { Shell } from "@/components/Shell";
import { TokenGate } from "@/components/TokenGate";
import { sectionFromPath } from "@/lib/nav";
const TOKEN_KEY = "handler_token";
export function AppFrame({ children }: { children: React.ReactNode }) {
const [token, setToken] = useState<string | null>(null);
const [error, setError] = useState("");
const pathname = usePathname();
// Read the stored token after mount (localStorage is client-only).
useEffect(() => {
const stored = window.localStorage.getItem(TOKEN_KEY);
if (stored) setToken(stored);
}, []);
const saveToken = useCallback((t: string) => {
window.localStorage.setItem(TOKEN_KEY, t);
setError("");
setToken(t);
}, []);
const signOut = useCallback(() => {
window.localStorage.removeItem(TOKEN_KEY);
setToken(null);
}, []);
const onUnauthorized = useCallback(() => {
window.localStorage.removeItem(TOKEN_KEY);
setToken(null);
setError("Invalid token — please try again.");
}, []);
if (!token) {
return <TokenGate error={error} onSubmit={saveToken} />;
}
return (
<DashboardProvider
token={token}
onUnauthorized={onUnauthorized}
initialSection={sectionFromPath(pathname)}
>
<Shell onSignOut={signOut}>{children}</Shell>
</DashboardProvider>
);
}