/** * dashboard-client/src/tabs/dashHashRouter.ts — DASH-0d additive hash→surface * deep-link router. * * The dashboard has historically had NO URL-routing (single `useState * ("overview")` in App.tsx). DASH-0d adds an ADDITIVE hash listener so every * legacy deep link (e.g. `#turns`, `#metrics`, `#repos`) still lands on a live * consolidated surface, and an empty hash keeps the current behavior (the * caller's default tab). It does NOT replace the existing tab state — App.tsx * keeps `useState` and this hook only *points* at a surface when the hash maps * to one. * * Mapping (DASH-0a `DEEP_LINK_TARGETS` + the DASH-0d release set): * #sessions, #turns → sessions * #cache, #metrics → cache-perf * #repos, #wiki, #memory-map → memory-graph * #events, #health, #vector-cortex → diagnostics * #maintenance, #config → admin * #setup, #overview → themselves * (empty) → null (current behavior) * * PREVENT-PI-004: browser `location` + `hashchange` only — no network. * PREVENT-011: no `any`. Pure client view concern; no endpoint reads added. */ import { useEffect, useState } from "react"; import type { DashTabId } from "./registry"; /** * The consolidated surface a legacy hash resolves to. Every key is a legacy * deep-link fragment mapped onto the fixed 7 surfaces. `config`/`setup`/ * `overview` are explicit release aliases so no old bookmark goes dead. */ export const HASH_TO_SURFACE: Readonly> = { overview: "overview", sessions: "sessions", turns: "sessions", cache: "cache-perf", metrics: "cache-perf", "memory-map": "memory-graph", repos: "memory-graph", wiki: "memory-graph", "vector-cortex": "diagnostics", events: "diagnostics", health: "diagnostics", setup: "setup", config: "admin", maintenance: "admin", }; /** Resolve the current `location.hash` to a consolidated surface (null if empty). */ export function resolveHashToSurface(hash: string): DashTabId | null { const clean = hash.replace(/^#/, "").trim(); if (!clean) return null; const surface = HASH_TO_SURFACE[clean]; return surface ?? null; } /** * Additive hash listener. Returns the consolidated `DashTabId` the current * `location.hash` maps to, or `null` when the hash is empty (current behavior) * or unmapped. Re-syncs whenever the hash changes. */ export function useHashTab(): DashTabId | null { const [surface, setSurface] = useState(() => resolveHashToSurface(window.location.hash), ); useEffect(() => { const onHashChange = () => setSurface(resolveHashToSurface(window.location.hash)); window.addEventListener("hashchange", onHashChange); return () => window.removeEventListener("hashchange", onHashChange); }, []); return surface; }