277 lines
11 KiB
TypeScript
277 lines
11 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useState } from 'react'
|
|
import { usePathname, useRouter } from 'next/navigation'
|
|
import { NavRail } from '@/components/layout/nav-rail'
|
|
import { HeaderBar } from '@/components/layout/header-bar'
|
|
import { LiveFeed } from '@/components/layout/live-feed'
|
|
import { Dashboard } from '@/components/dashboard/dashboard'
|
|
import { AgentSpawnPanel } from '@/components/panels/agent-spawn-panel'
|
|
import { LogViewerPanel } from '@/components/panels/log-viewer-panel'
|
|
import { CronManagementPanel } from '@/components/panels/cron-management-panel'
|
|
import { MemoryBrowserPanel } from '@/components/panels/memory-browser-panel'
|
|
import { TokenDashboardPanel } from '@/components/panels/token-dashboard-panel'
|
|
import { AgentCostPanel } from '@/components/panels/agent-cost-panel'
|
|
import { SessionDetailsPanel } from '@/components/panels/session-details-panel'
|
|
import { TaskBoardPanel } from '@/components/panels/task-board-panel'
|
|
import { ActivityFeedPanel } from '@/components/panels/activity-feed-panel'
|
|
import { AgentSquadPanelPhase3 } from '@/components/panels/agent-squad-panel-phase3'
|
|
import { AgentCommsPanel } from '@/components/panels/agent-comms-panel'
|
|
import { StandupPanel } from '@/components/panels/standup-panel'
|
|
import { OrchestrationBar } from '@/components/panels/orchestration-bar'
|
|
import { NotificationsPanel } from '@/components/panels/notifications-panel'
|
|
import { UserManagementPanel } from '@/components/panels/user-management-panel'
|
|
import { AuditTrailPanel } from '@/components/panels/audit-trail-panel'
|
|
import { AgentHistoryPanel } from '@/components/panels/agent-history-panel'
|
|
import { WebhookPanel } from '@/components/panels/webhook-panel'
|
|
import { SettingsPanel } from '@/components/panels/settings-panel'
|
|
import { GatewayConfigPanel } from '@/components/panels/gateway-config-panel'
|
|
import { IntegrationsPanel } from '@/components/panels/integrations-panel'
|
|
import { AlertRulesPanel } from '@/components/panels/alert-rules-panel'
|
|
import { MultiGatewayPanel } from '@/components/panels/multi-gateway-panel'
|
|
import { SuperAdminPanel } from '@/components/panels/super-admin-panel'
|
|
import { OfficePanel } from '@/components/panels/office-panel'
|
|
import { GitHubSyncPanel } from '@/components/panels/github-sync-panel'
|
|
import { ChatPanel } from '@/components/chat/chat-panel'
|
|
import { ErrorBoundary } from '@/components/ErrorBoundary'
|
|
import { LocalModeBanner } from '@/components/layout/local-mode-banner'
|
|
import { UpdateBanner } from '@/components/layout/update-banner'
|
|
import { PromoBanner } from '@/components/layout/promo-banner'
|
|
import { useWebSocket } from '@/lib/websocket'
|
|
import { useServerEvents } from '@/lib/use-server-events'
|
|
import { useMissionControl } from '@/store'
|
|
|
|
export default function Home() {
|
|
const router = useRouter()
|
|
const { connect } = useWebSocket()
|
|
const { activeTab, setActiveTab, setCurrentUser, setDashboardMode, setGatewayAvailable, setSubscription, setUpdateAvailable, 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()
|
|
const [isClient, setIsClient] = useState(false)
|
|
|
|
useEffect(() => {
|
|
setIsClient(true)
|
|
|
|
// Fetch current user
|
|
fetch('/api/auth/me')
|
|
.then(async (res) => {
|
|
if (res.ok) return res.json()
|
|
if (res.status === 401) {
|
|
router.replace(`/login?next=${encodeURIComponent(pathname)}`)
|
|
}
|
|
return null
|
|
})
|
|
.then(data => { if (data?.user) setCurrentUser(data.user) })
|
|
.catch(() => {})
|
|
|
|
// Check for available updates
|
|
fetch('/api/releases/check')
|
|
.then(res => res.ok ? res.json() : null)
|
|
.then(data => {
|
|
if (data?.updateAvailable) {
|
|
setUpdateAvailable({
|
|
latestVersion: data.latestVersion,
|
|
releaseUrl: data.releaseUrl,
|
|
releaseNotes: data.releaseNotes,
|
|
})
|
|
}
|
|
})
|
|
.catch(() => {})
|
|
|
|
// Check capabilities, then conditionally connect to gateway
|
|
fetch('/api/status?action=capabilities')
|
|
.then(res => res.ok ? res.json() : null)
|
|
.then(data => {
|
|
if (data?.subscription) {
|
|
setSubscription(data.subscription)
|
|
}
|
|
if (data && data.gateway === false) {
|
|
setDashboardMode('local')
|
|
setGatewayAvailable(false)
|
|
// Skip WebSocket connect — no gateway to talk to
|
|
return
|
|
}
|
|
if (data && data.gateway === true) {
|
|
setDashboardMode('full')
|
|
setGatewayAvailable(true)
|
|
}
|
|
// Connect to gateway WebSocket
|
|
const wsToken = process.env.NEXT_PUBLIC_GATEWAY_TOKEN || process.env.NEXT_PUBLIC_WS_TOKEN || ''
|
|
const explicitWsUrl = process.env.NEXT_PUBLIC_GATEWAY_URL || ''
|
|
const gatewayPort = process.env.NEXT_PUBLIC_GATEWAY_PORT || '18789'
|
|
const gatewayHost = process.env.NEXT_PUBLIC_GATEWAY_HOST || window.location.hostname
|
|
const gatewayProto =
|
|
process.env.NEXT_PUBLIC_GATEWAY_PROTOCOL ||
|
|
(window.location.protocol === 'https:' ? 'wss' : 'ws')
|
|
const wsUrl = explicitWsUrl || `${gatewayProto}://${gatewayHost}:${gatewayPort}`
|
|
connect(wsUrl, wsToken)
|
|
})
|
|
.catch(() => {
|
|
// If capabilities check fails, still try to connect
|
|
const wsToken = process.env.NEXT_PUBLIC_GATEWAY_TOKEN || process.env.NEXT_PUBLIC_WS_TOKEN || ''
|
|
const explicitWsUrl = process.env.NEXT_PUBLIC_GATEWAY_URL || ''
|
|
const gatewayPort = process.env.NEXT_PUBLIC_GATEWAY_PORT || '18789'
|
|
const gatewayHost = process.env.NEXT_PUBLIC_GATEWAY_HOST || window.location.hostname
|
|
const gatewayProto =
|
|
process.env.NEXT_PUBLIC_GATEWAY_PROTOCOL ||
|
|
(window.location.protocol === 'https:' ? 'wss' : 'ws')
|
|
const wsUrl = explicitWsUrl || `${gatewayProto}://${gatewayHost}:${gatewayPort}`
|
|
connect(wsUrl, wsToken)
|
|
})
|
|
}, [connect, pathname, router, setCurrentUser, setDashboardMode, setGatewayAvailable, setSubscription, setUpdateAvailable])
|
|
|
|
if (!isClient) {
|
|
return (
|
|
<div className="flex items-center justify-center min-h-screen">
|
|
<div className="flex flex-col items-center gap-3">
|
|
<div className="w-10 h-10 rounded-xl bg-primary flex items-center justify-center">
|
|
<span className="text-primary-foreground font-bold text-sm">MC</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-1.5 h-1.5 rounded-full bg-primary animate-pulse" />
|
|
<span className="text-sm text-muted-foreground">Loading Mission Control...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="flex h-screen bg-background overflow-hidden">
|
|
<a href="#main-content" className="sr-only focus:not-sr-only focus:absolute focus:z-50 focus:top-2 focus:left-2 focus:px-4 focus:py-2 focus:bg-primary focus:text-primary-foreground focus:rounded-md focus:text-sm focus:font-medium">
|
|
Skip to main content
|
|
</a>
|
|
{/* Left: Icon rail navigation (hidden on mobile, shown as bottom bar instead) */}
|
|
<NavRail />
|
|
|
|
{/* Center: Header + Content */}
|
|
<div className="flex-1 flex flex-col min-w-0">
|
|
<HeaderBar />
|
|
<LocalModeBanner />
|
|
<UpdateBanner />
|
|
<PromoBanner />
|
|
<main id="main-content" className="flex-1 overflow-auto pb-16 md:pb-0" role="main">
|
|
<div aria-live="polite">
|
|
<ErrorBoundary key={activeTab}>
|
|
<ContentRouter tab={activeTab} />
|
|
</ErrorBoundary>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
{/* Right: Live feed (hidden on mobile) */}
|
|
{liveFeedOpen && (
|
|
<div className="hidden lg:flex h-full">
|
|
<LiveFeed />
|
|
</div>
|
|
)}
|
|
|
|
{/* Floating button to reopen LiveFeed when closed */}
|
|
{!liveFeedOpen && (
|
|
<button
|
|
onClick={toggleLiveFeed}
|
|
className="hidden lg:flex fixed right-0 top-1/2 -translate-y-1/2 z-30 w-6 h-12 items-center justify-center bg-card border border-r-0 border-border rounded-l-md text-muted-foreground hover:text-foreground hover:bg-secondary transition-all duration-200"
|
|
title="Show live feed"
|
|
>
|
|
<svg className="w-3.5 h-3.5" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
<path d="M10 3l-5 5 5 5" strokeLinecap="round" strokeLinejoin="round" />
|
|
</svg>
|
|
</button>
|
|
)}
|
|
|
|
{/* Chat panel overlay */}
|
|
<ChatPanel />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function ContentRouter({ tab }: { tab: string }) {
|
|
const { dashboardMode } = useMissionControl()
|
|
const isLocal = dashboardMode === 'local'
|
|
|
|
switch (tab) {
|
|
case 'overview':
|
|
return (
|
|
<>
|
|
<Dashboard />
|
|
{!isLocal && (
|
|
<div className="mt-4 mx-4 mb-4 rounded-xl border border-border bg-card overflow-hidden">
|
|
<AgentCommsPanel />
|
|
</div>
|
|
)}
|
|
</>
|
|
)
|
|
case 'tasks':
|
|
return <TaskBoardPanel />
|
|
case 'agents':
|
|
return (
|
|
<>
|
|
<OrchestrationBar />
|
|
<AgentSquadPanelPhase3 />
|
|
{!isLocal && (
|
|
<div className="mt-4 mx-4 mb-4 rounded-xl border border-border bg-card overflow-hidden">
|
|
<AgentCommsPanel />
|
|
</div>
|
|
)}
|
|
</>
|
|
)
|
|
case 'activity':
|
|
return <ActivityFeedPanel />
|
|
case 'notifications':
|
|
return <NotificationsPanel />
|
|
case 'standup':
|
|
return <StandupPanel />
|
|
case 'spawn':
|
|
return <AgentSpawnPanel />
|
|
case 'sessions':
|
|
return <SessionDetailsPanel />
|
|
case 'logs':
|
|
return <LogViewerPanel />
|
|
case 'cron':
|
|
return <CronManagementPanel />
|
|
case 'memory':
|
|
return <MemoryBrowserPanel />
|
|
case 'tokens':
|
|
return <TokenDashboardPanel />
|
|
case 'agent-costs':
|
|
return <AgentCostPanel />
|
|
case 'users':
|
|
return <UserManagementPanel />
|
|
case 'history':
|
|
return <AgentHistoryPanel />
|
|
case 'audit':
|
|
return <AuditTrailPanel />
|
|
case 'webhooks':
|
|
return <WebhookPanel />
|
|
case 'alerts':
|
|
return <AlertRulesPanel />
|
|
case 'gateways':
|
|
return <MultiGatewayPanel />
|
|
case 'gateway-config':
|
|
return <GatewayConfigPanel />
|
|
case 'integrations':
|
|
return <IntegrationsPanel />
|
|
case 'settings':
|
|
return <SettingsPanel />
|
|
case 'github':
|
|
return <GitHubSyncPanel />
|
|
case 'office':
|
|
return <OfficePanel />
|
|
case 'super-admin':
|
|
return <SuperAdminPanel />
|
|
case 'workspaces':
|
|
return <SuperAdminPanel />
|
|
default:
|
|
return <Dashboard />
|
|
}
|
|
}
|