diff --git a/packages/dashboard/src/app.tsx b/packages/dashboard/src/app.tsx index 0d65b8c..5ee4013 100644 --- a/packages/dashboard/src/app.tsx +++ b/packages/dashboard/src/app.tsx @@ -5,12 +5,10 @@ import { StatusBar } from "./components/status-bar.tsx"; import { ThreadDetail } from "./components/thread-detail.tsx"; import { ThreadList } from "./components/thread-list.tsx"; import { WorkflowList } from "./components/workflow-list.tsx"; - -type View = "threads" | "workflows"; +import { useHashRoute } from "./use-hash-route.ts"; export function App() { - const [view, setView] = useState("threads"); - const [selectedThread, setSelectedThread] = useState(null); + const { view, threadId, setView, setThreadId } = useHashRoute(); const [showRun, setShowRun] = useState(false); return ( @@ -19,9 +17,9 @@ export function App() {
setShowRun(true)} />
- {view === "threads" && !selectedThread && } - {view === "threads" && selectedThread && ( - setSelectedThread(null)} /> + {view === "threads" && threadId === null && } + {view === "threads" && threadId !== null && ( + setThreadId(null)} /> )} {view === "workflows" && }
@@ -31,8 +29,7 @@ export function App() { onClose={() => setShowRun(false)} onCreated={(id) => { setShowRun(false); - setView("threads"); - setSelectedThread(id); + setThreadId(id); }} /> )} diff --git a/packages/dashboard/src/components/status-bar.tsx b/packages/dashboard/src/components/status-bar.tsx index 69e81bf..7ed360b 100644 --- a/packages/dashboard/src/components/status-bar.tsx +++ b/packages/dashboard/src/components/status-bar.tsx @@ -1,12 +1,47 @@ +import { useCallback, useEffect, useRef, useState } from "react"; import { getHealth } from "../api.ts"; -import { useFetch } from "../hooks.ts"; + +type HealthStatus = "connected" | "disconnected" | "reconnecting"; type Props = { onRun: () => void; }; +function statusLabel(status: HealthStatus): { text: string; color: string } { + if (status === "connected") { + return { text: "● Connected", color: "var(--color-success)" }; + } + if (status === "reconnecting") { + return { text: "● Reconnecting...", color: "var(--color-warning, #f59e0b)" }; + } + return { text: "● Offline", color: "var(--color-error)" }; +} + export function StatusBar({ onRun }: Props) { - const health = useFetch(() => getHealth(), []); + const [status, setStatus] = useState("disconnected"); + const wasConnectedRef = useRef(false); + + const checkHealth = useCallback(async () => { + try { + await getHealth(); + wasConnectedRef.current = true; + setStatus("connected"); + } catch { + if (wasConnectedRef.current) { + setStatus("reconnecting"); + } else { + setStatus("disconnected"); + } + } + }, []); + + useEffect(() => { + checkHealth(); + const interval = setInterval(checkHealth, 10_000); + return () => clearInterval(interval); + }, [checkHealth]); + + const label = statusLabel(status); return (
- - {health.status === "loading" && "⏳ Connecting..."} - {health.status === "ok" && ( - ● Connected - )} - {health.status === "error" && ( - ● Offline - )} - + {label.text} ); } diff --git a/packages/dashboard/src/use-hash-route.ts b/packages/dashboard/src/use-hash-route.ts new file mode 100644 index 0000000..aa2ed73 --- /dev/null +++ b/packages/dashboard/src/use-hash-route.ts @@ -0,0 +1,64 @@ +import { useCallback, useEffect, useState } from "react"; + +type View = "threads" | "workflows"; + +type HashRoute = { + view: View; + threadId: string | null; +}; + +function parseHash(hash: string): HashRoute { + const raw = hash.replace(/^#\/?/, ""); + if (raw.startsWith("threads/")) { + const id = raw.slice("threads/".length); + if (id.length > 0) { + return { view: "threads", threadId: id }; + } + } + if (raw === "workflows") { + return { view: "workflows", threadId: null }; + } + return { view: "threads", threadId: null }; +} + +function buildHash(route: HashRoute): string { + if (route.view === "workflows") { + return "#workflows"; + } + if (route.threadId !== null) { + return `#threads/${route.threadId}`; + } + return "#threads"; +} + +export function useHashRoute(): { + view: View; + threadId: string | null; + setView: (v: View) => void; + setThreadId: (id: string | null) => void; +} { + const [route, setRoute] = useState(() => parseHash(window.location.hash)); + + useEffect(() => { + function onHashChange(): void { + setRoute(parseHash(window.location.hash)); + } + window.addEventListener("hashchange", onHashChange); + return () => window.removeEventListener("hashchange", onHashChange); + }, []); + + const navigate = useCallback((next: HashRoute) => { + const hash = buildHash(next); + window.location.hash = hash; + setRoute(next); + }, []); + + const setView = useCallback((v: View) => navigate({ view: v, threadId: null }), [navigate]); + + const setThreadId = useCallback( + (id: string | null) => navigate({ view: "threads", threadId: id }), + [navigate], + ); + + return { view: route.view, threadId: route.threadId, setView, setThreadId }; +}