Move page.tsx to [[...panel]] optional catch-all route so each panel gets its own URL (e.g. /tasks, /agents, /settings). URL is the source of truth — synced into Zustand via usePathname on every navigation. Enables bookmarking, refresh persistence, deep-linking, and browser back/forward.
This commit is contained in:
parent
a6fb27392b
commit
6ce38b13dc
|
|
@ -1,6 +1,7 @@
|
|||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { NavRail } from '@/components/layout/nav-rail'
|
||||
import { HeaderBar } from '@/components/layout/header-bar'
|
||||
import { LiveFeed } from '@/components/layout/live-feed'
|
||||
|
|
@ -39,7 +40,15 @@ import { useMissionControl } from '@/store'
|
|||
|
||||
export default function Home() {
|
||||
const { connect } = useWebSocket()
|
||||
const { activeTab, setCurrentUser, setDashboardMode, setGatewayAvailable, setSubscription, liveFeedOpen, toggleLiveFeed } = useMissionControl()
|
||||
const { activeTab, setActiveTab, setCurrentUser, setDashboardMode, setGatewayAvailable, setSubscription, liveFeedOpen, toggleLiveFeed } = useMissionControl()
|
||||
|
||||
// Sync URL → Zustand activeTab
|
||||
const pathname = usePathname()
|
||||
const panelFromUrl = pathname === '/' ? 'overview' : pathname.slice(1)
|
||||
|
||||
useEffect(() => {
|
||||
setActiveTab(panelFromUrl)
|
||||
}, [panelFromUrl, setActiveTab])
|
||||
|
||||
// Connect to SSE for real-time local DB events (tasks, agents, chat, etc.)
|
||||
useServerEvents()
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { useState, useCallback } from 'react'
|
||||
import { useMissionControl } from '@/store'
|
||||
import { useNavigateToPanel } from '@/lib/navigation'
|
||||
import { useSmartPoll } from '@/lib/use-smart-poll'
|
||||
|
||||
interface DbStats {
|
||||
|
|
@ -35,8 +36,8 @@ export function Dashboard() {
|
|||
logs,
|
||||
agents,
|
||||
tasks,
|
||||
setActiveTab,
|
||||
} = useMissionControl()
|
||||
const navigateToPanel = useNavigateToPanel()
|
||||
const isLocal = dashboardMode === 'local'
|
||||
const subscriptionLabel = subscription?.type
|
||||
? subscription.type.charAt(0).toUpperCase() + subscription.type.slice(1)
|
||||
|
|
@ -124,7 +125,7 @@ export function Dashboard() {
|
|||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
{isLocal ? (
|
||||
<>
|
||||
<div className="cursor-pointer" onClick={() => setActiveTab('sessions')}>
|
||||
<div className="cursor-pointer" onClick={() => navigateToPanel('sessions')}>
|
||||
<MetricCard
|
||||
label="Active Sessions"
|
||||
value={claudeStats?.active_sessions ?? activeSessions}
|
||||
|
|
@ -133,7 +134,7 @@ export function Dashboard() {
|
|||
color="blue"
|
||||
/>
|
||||
</div>
|
||||
<div className="cursor-pointer" onClick={() => setActiveTab('sessions')}>
|
||||
<div className="cursor-pointer" onClick={() => navigateToPanel('sessions')}>
|
||||
<MetricCard
|
||||
label="Projects"
|
||||
value={claudeStats?.unique_projects ?? 0}
|
||||
|
|
@ -141,7 +142,7 @@ export function Dashboard() {
|
|||
color="green"
|
||||
/>
|
||||
</div>
|
||||
<div className="cursor-pointer" onClick={() => setActiveTab('tokens')}>
|
||||
<div className="cursor-pointer" onClick={() => navigateToPanel('tokens')}>
|
||||
<MetricCard
|
||||
label="Tokens Used"
|
||||
value={formatTokensShort((claudeStats?.total_input_tokens ?? 0) + (claudeStats?.total_output_tokens ?? 0))}
|
||||
|
|
@ -150,7 +151,7 @@ export function Dashboard() {
|
|||
color="purple"
|
||||
/>
|
||||
</div>
|
||||
<div className="cursor-pointer" onClick={() => setActiveTab('tokens')}>
|
||||
<div className="cursor-pointer" onClick={() => navigateToPanel('tokens')}>
|
||||
<MetricCard
|
||||
label="Est. Cost"
|
||||
value={subscriptionLabel ? `Included` : `$${(claudeStats?.total_estimated_cost ?? 0).toFixed(2)}`}
|
||||
|
|
@ -162,7 +163,7 @@ export function Dashboard() {
|
|||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="cursor-pointer" onClick={() => setActiveTab('history')}>
|
||||
<div className="cursor-pointer" onClick={() => navigateToPanel('history')}>
|
||||
<MetricCard
|
||||
label="Active Sessions"
|
||||
value={activeSessions}
|
||||
|
|
@ -171,7 +172,7 @@ export function Dashboard() {
|
|||
color="blue"
|
||||
/>
|
||||
</div>
|
||||
<div className="cursor-pointer" onClick={() => setActiveTab('agents')}>
|
||||
<div className="cursor-pointer" onClick={() => navigateToPanel('agents')}>
|
||||
<MetricCard
|
||||
label="Agents Online"
|
||||
value={onlineAgents}
|
||||
|
|
@ -180,7 +181,7 @@ export function Dashboard() {
|
|||
color="green"
|
||||
/>
|
||||
</div>
|
||||
<div className="cursor-pointer" onClick={() => setActiveTab('tasks')}>
|
||||
<div className="cursor-pointer" onClick={() => navigateToPanel('tasks')}>
|
||||
<MetricCard
|
||||
label="Tasks Running"
|
||||
value={runningTasks}
|
||||
|
|
@ -189,7 +190,7 @@ export function Dashboard() {
|
|||
color="purple"
|
||||
/>
|
||||
</div>
|
||||
<div className="cursor-pointer" onClick={() => setActiveTab('logs')}>
|
||||
<div className="cursor-pointer" onClick={() => navigateToPanel('logs')}>
|
||||
<MetricCard
|
||||
label="Errors (24h)"
|
||||
value={errorCount}
|
||||
|
|
@ -261,7 +262,7 @@ export function Dashboard() {
|
|||
|
||||
{/* Middle panel: Claude Stats (local) or Security & Audit (full) */}
|
||||
{isLocal ? (
|
||||
<div className="panel cursor-pointer hover:border-primary/30 transition-smooth" onClick={() => setActiveTab('sessions')}>
|
||||
<div className="panel cursor-pointer hover:border-primary/30 transition-smooth" onClick={() => navigateToPanel('sessions')}>
|
||||
<div className="panel-header">
|
||||
<h3 className="text-sm font-semibold text-foreground">Claude Code Stats</h3>
|
||||
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-2xs font-medium bg-purple-500/10 text-purple-400 border border-purple-500/20">
|
||||
|
|
@ -305,7 +306,7 @@ export function Dashboard() {
|
|||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="panel cursor-pointer hover:border-primary/30 transition-smooth" onClick={() => setActiveTab('audit')}>
|
||||
<div className="panel cursor-pointer hover:border-primary/30 transition-smooth" onClick={() => navigateToPanel('audit')}>
|
||||
<div className="panel-header">
|
||||
<h3 className="text-sm font-semibold text-foreground">Security & Audit</h3>
|
||||
{dbStats && dbStats.audit.loginFailures > 0 && (
|
||||
|
|
@ -513,15 +514,15 @@ export function Dashboard() {
|
|||
{/* Quick Actions */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-5 gap-2">
|
||||
{!isLocal && (
|
||||
<QuickAction label="Spawn Agent" desc="Launch sub-agent" tab="spawn" icon={<SpawnActionIcon />} setActiveTab={setActiveTab} />
|
||||
<QuickAction label="Spawn Agent" desc="Launch sub-agent" tab="spawn" icon={<SpawnActionIcon />} onNavigate={navigateToPanel} />
|
||||
)}
|
||||
<QuickAction label="View Logs" desc="Real-time viewer" tab="logs" icon={<LogActionIcon />} setActiveTab={setActiveTab} />
|
||||
<QuickAction label="Task Board" desc="Kanban view" tab="tasks" icon={<TaskActionIcon />} setActiveTab={setActiveTab} />
|
||||
<QuickAction label="Memory" desc="Knowledge base" tab="memory" icon={<MemoryActionIcon />} setActiveTab={setActiveTab} />
|
||||
<QuickAction label="View Logs" desc="Real-time viewer" tab="logs" icon={<LogActionIcon />} onNavigate={navigateToPanel} />
|
||||
<QuickAction label="Task Board" desc="Kanban view" tab="tasks" icon={<TaskActionIcon />} onNavigate={navigateToPanel} />
|
||||
<QuickAction label="Memory" desc="Knowledge base" tab="memory" icon={<MemoryActionIcon />} onNavigate={navigateToPanel} />
|
||||
{isLocal ? (
|
||||
<QuickAction label="Sessions" desc="Claude Code sessions" tab="sessions" icon={<SessionIcon />} setActiveTab={setActiveTab} />
|
||||
<QuickAction label="Sessions" desc="Claude Code sessions" tab="sessions" icon={<SessionIcon />} onNavigate={navigateToPanel} />
|
||||
) : (
|
||||
<QuickAction label="Orchestration" desc="Workflows & pipelines" tab="orchestration" icon={<PipelineActionIcon />} setActiveTab={setActiveTab} />
|
||||
<QuickAction label="Orchestration" desc="Workflows & pipelines" tab="orchestration" icon={<PipelineActionIcon />} onNavigate={navigateToPanel} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -616,13 +617,13 @@ function StatusBadge({ connected }: { connected: boolean }) {
|
|||
)
|
||||
}
|
||||
|
||||
function QuickAction({ label, desc, tab, icon, setActiveTab }: {
|
||||
function QuickAction({ label, desc, tab, icon, onNavigate }: {
|
||||
label: string; desc: string; tab: string; icon: React.ReactNode
|
||||
setActiveTab: (tab: string) => void
|
||||
onNavigate: (tab: string) => void
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
onClick={() => setActiveTab(tab)}
|
||||
onClick={() => onNavigate(tab)}
|
||||
className="flex items-center gap-3 p-3 rounded-lg border border-border hover:border-primary/30 hover:bg-primary/5 transition-smooth text-left group"
|
||||
>
|
||||
<div className="w-8 h-8 rounded-md bg-secondary flex items-center justify-center shrink-0 group-hover:bg-primary/10 transition-smooth">
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useMissionControl } from '@/store'
|
||||
import { useNavigateToPanel } from '@/lib/navigation'
|
||||
|
||||
interface MenuItem {
|
||||
id: string
|
||||
|
|
@ -26,7 +27,8 @@ const menuItems: MenuItem[] = [
|
|||
]
|
||||
|
||||
export function Sidebar() {
|
||||
const { activeTab, setActiveTab, connection, sessions } = useMissionControl()
|
||||
const { activeTab, connection, sessions } = useMissionControl()
|
||||
const navigateToPanel = useNavigateToPanel()
|
||||
const [systemStats, setSystemStats] = useState<any>(null)
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -61,7 +63,7 @@ export function Sidebar() {
|
|||
{menuItems.map((item) => (
|
||||
<li key={item.id}>
|
||||
<button
|
||||
onClick={() => setActiveTab(item.id)}
|
||||
onClick={() => navigateToPanel(item.id)}
|
||||
className={`w-full flex items-start space-x-3 px-3 py-3 rounded-lg text-left transition-colors group ${
|
||||
activeTab === item.id
|
||||
? 'bg-primary text-primary-foreground shadow-sm'
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useState, useRef, useEffect, useCallback } from 'react'
|
|||
import { useRouter } from 'next/navigation'
|
||||
import { useMissionControl } from '@/store'
|
||||
import { useWebSocket } from '@/lib/websocket'
|
||||
import { useNavigateToPanel } from '@/lib/navigation'
|
||||
import { ThemeToggle } from '@/components/ui/theme-toggle'
|
||||
import { DigitalClock } from '@/components/ui/digital-clock'
|
||||
|
||||
|
|
@ -17,8 +18,9 @@ interface SearchResult {
|
|||
}
|
||||
|
||||
export function HeaderBar() {
|
||||
const { activeTab, setActiveTab, connection, sessions, chatPanelOpen, setChatPanelOpen, notifications, unreadNotificationCount, currentUser, setCurrentUser } = useMissionControl()
|
||||
const { activeTab, connection, sessions, chatPanelOpen, setChatPanelOpen, notifications, unreadNotificationCount, currentUser, setCurrentUser } = useMissionControl()
|
||||
const { isConnected, reconnect } = useWebSocket()
|
||||
const navigateToPanel = useNavigateToPanel()
|
||||
|
||||
const activeSessions = sessions.filter(s => s.active).length
|
||||
const tabLabels: Record<string, string> = {
|
||||
|
|
@ -105,7 +107,7 @@ export function HeaderBar() {
|
|||
audit: 'audit', message: 'agents', notification: 'notifications',
|
||||
webhook: 'webhooks', pipeline: 'agents', alert_rule: 'alerts',
|
||||
}
|
||||
setActiveTab(typeToTab[result.type] || 'overview')
|
||||
navigateToPanel(typeToTab[result.type] || 'overview')
|
||||
setSearchOpen(false)
|
||||
setSearchQuery('')
|
||||
setSearchResults([])
|
||||
|
|
@ -187,10 +189,7 @@ export function HeaderBar() {
|
|||
|
||||
{/* Notifications */}
|
||||
<button
|
||||
onClick={() => {
|
||||
const { setActiveTab } = useMissionControl.getState()
|
||||
setActiveTab('notifications')
|
||||
}}
|
||||
onClick={() => navigateToPanel('notifications')}
|
||||
className="h-8 w-8 rounded-md text-muted-foreground hover:text-foreground hover:bg-secondary transition-smooth flex items-center justify-center relative"
|
||||
>
|
||||
<BellIcon />
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
'use client'
|
||||
|
||||
import { useMissionControl } from '@/store'
|
||||
import { useNavigateToPanel } from '@/lib/navigation'
|
||||
|
||||
export function LocalModeBanner() {
|
||||
const { dashboardMode, bannerDismissed, dismissBanner, setActiveTab } = useMissionControl()
|
||||
const { dashboardMode, bannerDismissed, dismissBanner } = useMissionControl()
|
||||
const navigateToPanel = useNavigateToPanel()
|
||||
|
||||
if (dashboardMode === 'full' || bannerDismissed) return null
|
||||
|
||||
|
|
@ -15,7 +17,7 @@ export function LocalModeBanner() {
|
|||
{' — running in Local Mode. Monitoring Claude Code sessions, tasks, and local data.'}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setActiveTab('gateways')}
|
||||
onClick={() => navigateToPanel('gateways')}
|
||||
className="shrink-0 text-2xs font-medium text-blue-400 hover:text-blue-300 px-2 py-1 rounded border border-blue-500/20 hover:border-blue-500/40 transition-colors"
|
||||
>
|
||||
Configure Gateway
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useMissionControl } from '@/store'
|
||||
import { useNavigateToPanel } from '@/lib/navigation'
|
||||
|
||||
interface NavItem {
|
||||
id: string
|
||||
|
|
@ -69,7 +70,8 @@ const navGroups: NavGroup[] = [
|
|||
const allNavItems = navGroups.flatMap(g => g.items)
|
||||
|
||||
export function NavRail() {
|
||||
const { activeTab, setActiveTab, connection, dashboardMode, sidebarExpanded, collapsedGroups, toggleSidebar, toggleGroup } = useMissionControl()
|
||||
const { activeTab, connection, dashboardMode, sidebarExpanded, collapsedGroups, toggleSidebar, toggleGroup } = useMissionControl()
|
||||
const navigateToPanel = useNavigateToPanel()
|
||||
const isLocal = dashboardMode === 'local'
|
||||
|
||||
// Keyboard shortcut: [ to toggle sidebar
|
||||
|
|
@ -167,7 +169,7 @@ export function NavRail() {
|
|||
active={activeTab === item.id}
|
||||
expanded={sidebarExpanded}
|
||||
disabled={disabled}
|
||||
onClick={() => { if (!disabled) setActiveTab(item.id) }}
|
||||
onClick={() => { if (!disabled) navigateToPanel(item.id) }}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
|
@ -196,7 +198,7 @@ export function NavRail() {
|
|||
</nav>
|
||||
|
||||
{/* Mobile: Bottom tab bar */}
|
||||
<MobileBottomBar activeTab={activeTab} setActiveTab={setActiveTab} />
|
||||
<MobileBottomBar activeTab={activeTab} navigateToPanel={navigateToPanel} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -257,9 +259,9 @@ function NavButton({ item, active, expanded, disabled, onClick }: {
|
|||
)
|
||||
}
|
||||
|
||||
function MobileBottomBar({ activeTab, setActiveTab }: {
|
||||
function MobileBottomBar({ activeTab, navigateToPanel }: {
|
||||
activeTab: string
|
||||
setActiveTab: (tab: string) => void
|
||||
navigateToPanel: (tab: string) => void
|
||||
}) {
|
||||
const [sheetOpen, setSheetOpen] = useState(false)
|
||||
const priorityItems = allNavItems.filter(i => i.priority)
|
||||
|
|
@ -273,7 +275,7 @@ function MobileBottomBar({ activeTab, setActiveTab }: {
|
|||
{priorityItems.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => setActiveTab(item.id)}
|
||||
onClick={() => navigateToPanel(item.id)}
|
||||
className={`flex flex-col items-center justify-center gap-0.5 px-2 py-2 rounded-lg transition-smooth min-w-[48px] min-h-[48px] ${
|
||||
activeTab === item.id
|
||||
? 'text-primary'
|
||||
|
|
@ -311,17 +313,17 @@ function MobileBottomBar({ activeTab, setActiveTab }: {
|
|||
open={sheetOpen}
|
||||
onClose={() => setSheetOpen(false)}
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
navigateToPanel={navigateToPanel}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function MobileBottomSheet({ open, onClose, activeTab, setActiveTab }: {
|
||||
function MobileBottomSheet({ open, onClose, activeTab, navigateToPanel }: {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
activeTab: string
|
||||
setActiveTab: (tab: string) => void
|
||||
navigateToPanel: (tab: string) => void
|
||||
}) {
|
||||
// Track mount state for animation
|
||||
const [visible, setVisible] = useState(false)
|
||||
|
|
@ -385,7 +387,7 @@ function MobileBottomSheet({ open, onClose, activeTab, setActiveTab }: {
|
|||
<button
|
||||
key={item.id}
|
||||
onClick={() => {
|
||||
setActiveTab(item.id)
|
||||
navigateToPanel(item.id)
|
||||
handleClose()
|
||||
}}
|
||||
className={`flex items-center gap-2.5 px-3 min-h-[48px] rounded-xl transition-smooth ${
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
'use client'
|
||||
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
export function panelHref(panel: string): string {
|
||||
return panel === 'overview' ? '/' : `/${panel}`
|
||||
}
|
||||
|
||||
export function useNavigateToPanel() {
|
||||
const router = useRouter()
|
||||
return useCallback((panel: string) => {
|
||||
router.push(panelHref(panel))
|
||||
}, [router])
|
||||
}
|
||||
Loading…
Reference in New Issue