mirror of
https://github.com/0xWheatyz/handler.git
synced 2026-08-30 17:36:24 +00:00
5d8c62450c
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
58 lines
1.8 KiB
TypeScript
58 lines
1.8 KiB
TypeScript
/* 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>
|
|
);
|
|
}
|