drive the headscale section from the url
sections were held in a usePanelChannel, which is exactly the opaque click the navigation audit catalogues: the id lived in an onClick closure, so a section could not be linked to, opened in a new tab or reached with the back button. /headscale/:section is now the source of truth. nav items are real NavLinks with the active class coming from the router rather than derived in js, and the screen redirects bare or unknown sections to a canonical url so the highlight always matches the address bar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,7 @@ export function App() {
|
||||
<Route path="/music" element={<Dashboard.MusicScreen />} />
|
||||
<Route path="/soulseek" element={<Dashboard.SoulseekScreen />} />
|
||||
<Route path="/headscale" element={<Dashboard.HeadscaleScreen />} />
|
||||
<Route path="/headscale/:section" element={<Dashboard.HeadscaleScreen />} />
|
||||
<Route path="/system-monitor" element={<Dashboard.SystemMonitorScreen />} />
|
||||
<Route path="/activity" element={<Dashboard.ActivityScreen />} />
|
||||
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { Navigate, useParams } from 'react-router';
|
||||
import type { LayoutNode } from 'officerdev';
|
||||
import { WorkspaceView } from 'officerdev';
|
||||
import { WorkspaceView, DEFAULT_HEADSCALE_SECTION, headscaleSectionPath, isHeadscaleSection } from 'officerdev';
|
||||
import { useDashboardState } from 'state/useDashboardState';
|
||||
import { defaultLayout } from './defaultLayout';
|
||||
|
||||
// /headscale uses the Workspace/Panel system (like /soulseek and /music): a section nav (headscale-nav) on
|
||||
// the left and a section view (headscale-view) on the right, coordinating via the 'headscale:section'
|
||||
// channel. Both talk to the officer-headscale sidecar through the /api/headscale auth proxy, which holds no
|
||||
// Headscale credentials of its own — the registered servers and their keys live in the sidecar.
|
||||
// the left and a section view (headscale-view) on the right. Both talk to the officer-headscale sidecar
|
||||
// through the /api/headscale auth proxy, which holds no Headscale credentials of its own — the registered
|
||||
// servers and their keys live in the sidecar.
|
||||
//
|
||||
// The open section is :section in the URL, so both panels read it with useParams instead of passing it
|
||||
// between themselves over a channel. This screen backs both /headscale and /headscale/:section and is the
|
||||
// single place that decides what an absent or bogus section means.
|
||||
|
||||
const ALLOWED_APP_TYPES = new Set<string | null>(['headscale-nav', 'headscale-view', null]);
|
||||
|
||||
@@ -24,6 +29,7 @@ function normalizeLayout(node: LayoutNode): LayoutNode {
|
||||
}
|
||||
|
||||
export const HeadscaleScreen = () => {
|
||||
const { section } = useParams();
|
||||
const rawWorkspace = useDashboardState<LayoutNode>('screens/headscale', defaultLayout);
|
||||
|
||||
const workspace = useMemo(() => {
|
||||
@@ -38,6 +44,13 @@ export const HeadscaleScreen = () => {
|
||||
}
|
||||
}, [rawWorkspace.isLoaded, workspace.value, rawWorkspace.value]);
|
||||
|
||||
// Bare /headscale, or a section that doesn't exist, resolves to a canonical URL rather than rendering a
|
||||
// default while the address bar says something else — the nav highlight is derived from the URL, so a URL
|
||||
// that names nothing would leave nothing highlighted.
|
||||
if (!isHeadscaleSection(section)) {
|
||||
return <Navigate to={headscaleSectionPath(DEFAULT_HEADSCALE_SECTION)} replace />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full w-full pt-2">
|
||||
<WorkspaceView workspace={workspace} locked />
|
||||
|
||||
Reference in New Issue
Block a user