fix(dashboard): content scroll overflow bug

This commit is contained in:
K7ZVX 2026-05-12 16:46:51 +00:00
commit 374fb835c5
4 changed files with 327 additions and 182 deletions

View file

@ -1,162 +1,162 @@
import { ReactNode, useEffect, useState } from 'react' import { ReactNode, useEffect, useState } from 'react'
import { Link, useLocation } from 'react-router-dom' import { Link, useLocation } from 'react-router-dom'
import { import {
LayoutDashboard, LayoutDashboard,
Radio, Radio,
Cloud, Cloud,
Settings, Settings,
Bell, Bell,
} from 'lucide-react' } from 'lucide-react'
import { fetchStatus, type SystemStatus } from '@/lib/api' import { fetchStatus, type SystemStatus } from '@/lib/api'
import { useWebSocket } from '@/hooks/useWebSocket' import { useWebSocket } from '@/hooks/useWebSocket'
interface LayoutProps { interface LayoutProps {
children: ReactNode children: ReactNode
} }
const navItems = [ const navItems = [
{ path: '/', label: 'Dashboard', icon: LayoutDashboard }, { path: '/', label: 'Dashboard', icon: LayoutDashboard },
{ path: '/mesh', label: 'Mesh', icon: Radio }, { path: '/mesh', label: 'Mesh', icon: Radio },
{ path: '/environment', label: 'Environment', icon: Cloud }, { path: '/environment', label: 'Environment', icon: Cloud },
{ path: '/config', label: 'Config', icon: Settings }, { path: '/config', label: 'Config', icon: Settings },
{ path: '/alerts', label: 'Alerts', icon: Bell }, { path: '/alerts', label: 'Alerts', icon: Bell },
] ]
function formatUptime(seconds: number): string { function formatUptime(seconds: number): string {
const days = Math.floor(seconds / 86400) const days = Math.floor(seconds / 86400)
const hours = Math.floor((seconds % 86400) / 3600) const hours = Math.floor((seconds % 86400) / 3600)
const mins = Math.floor((seconds % 3600) / 60) const mins = Math.floor((seconds % 3600) / 60)
if (days > 0) return `${days}d ${hours}h` if (days > 0) return `${days}d ${hours}h`
if (hours > 0) return `${hours}h ${mins}m` if (hours > 0) return `${hours}h ${mins}m`
return `${mins}m` return `${mins}m`
} }
function getPageTitle(pathname: string): string { function getPageTitle(pathname: string): string {
const item = navItems.find((i) => i.path === pathname) const item = navItems.find((i) => i.path === pathname)
return item?.label || 'Dashboard' return item?.label || 'Dashboard'
} }
export default function Layout({ children }: LayoutProps) { export default function Layout({ children }: LayoutProps) {
const location = useLocation() const location = useLocation()
const { connected } = useWebSocket() const { connected } = useWebSocket()
const [status, setStatus] = useState<SystemStatus | null>(null) const [status, setStatus] = useState<SystemStatus | null>(null)
const [currentTime, setCurrentTime] = useState(new Date()) const [currentTime, setCurrentTime] = useState(new Date())
useEffect(() => { useEffect(() => {
fetchStatus().then(setStatus).catch(console.error) fetchStatus().then(setStatus).catch(console.error)
const interval = setInterval(() => { const interval = setInterval(() => {
fetchStatus().then(setStatus).catch(console.error) fetchStatus().then(setStatus).catch(console.error)
}, 30000) }, 30000)
return () => clearInterval(interval) return () => clearInterval(interval)
}, []) }, [])
useEffect(() => { useEffect(() => {
const interval = setInterval(() => setCurrentTime(new Date()), 1000) const interval = setInterval(() => setCurrentTime(new Date()), 1000)
return () => clearInterval(interval) return () => clearInterval(interval)
}, []) }, [])
const timeStr = currentTime.toLocaleTimeString('en-US', { const timeStr = currentTime.toLocaleTimeString('en-US', {
hour12: false, hour12: false,
hour: '2-digit', hour: '2-digit',
minute: '2-digit', minute: '2-digit',
second: '2-digit', second: '2-digit',
}) })
return ( return (
<div className="flex min-h-screen bg-bg text-slate-200"> <div className="flex h-screen overflow-hidden bg-bg text-slate-200">
{/* Sidebar */} {/* Sidebar */}
<aside className="fixed left-0 top-0 h-screen w-[220px] bg-bg-card border-r border-border flex flex-col"> <aside className="w-[220px] flex-shrink-0 bg-bg-card border-r border-border flex flex-col overflow-y-auto">
{/* Logo */} {/* Logo */}
<div className="p-5 border-b border-border"> <div className="p-5 border-b border-border">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-blue-500 to-blue-700 flex items-center justify-center text-white font-bold text-xl"> <div className="w-10 h-10 rounded-lg bg-gradient-to-br from-blue-500 to-blue-700 flex items-center justify-center text-white font-bold text-xl">
M M
</div> </div>
<div> <div>
<div className="font-semibold text-lg">MeshAI</div> <div className="font-semibold text-lg">MeshAI</div>
<div className="text-xs text-slate-500 font-mono"> <div className="text-xs text-slate-500 font-mono">
v{status?.version || '...'} v{status?.version || '...'}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{/* Navigation */} {/* Navigation */}
<nav className="flex-1 py-4"> <nav className="flex-1 py-4">
{navItems.map((item) => { {navItems.map((item) => {
const isActive = location.pathname === item.path const isActive = location.pathname === item.path
const Icon = item.icon const Icon = item.icon
return ( return (
<Link <Link
key={item.path} key={item.path}
to={item.path} to={item.path}
className={`flex items-center gap-3 px-5 py-3 text-sm transition-colors relative ${ className={`flex items-center gap-3 px-5 py-3 text-sm transition-colors relative ${
isActive isActive
? 'text-blue-400 bg-blue-500/10' ? 'text-blue-400 bg-blue-500/10'
: 'text-slate-400 hover:text-slate-200 hover:bg-bg-hover' : 'text-slate-400 hover:text-slate-200 hover:bg-bg-hover'
}`} }`}
> >
{isActive && ( {isActive && (
<div className="absolute left-0 top-0 bottom-0 w-0.5 bg-blue-500" /> <div className="absolute left-0 top-0 bottom-0 w-0.5 bg-blue-500" />
)} )}
<Icon size={18} /> <Icon size={18} />
{item.label} {item.label}
</Link> </Link>
) )
})} })}
</nav> </nav>
{/* Connection status */} {/* Connection status */}
<div className="p-5 border-t border-border"> <div className="p-5 border-t border-border">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<div <div
className={`w-2 h-2 rounded-full ${ className={`w-2 h-2 rounded-full ${
status?.connected ? 'bg-green-500' : 'bg-red-500' status?.connected ? 'bg-green-500' : 'bg-red-500'
}`} }`}
/> />
<span className="text-xs text-slate-400"> <span className="text-xs text-slate-400">
{status?.connected ? 'Connected' : 'Disconnected'} {status?.connected ? 'Connected' : 'Disconnected'}
</span> </span>
</div> </div>
<div className="text-xs text-slate-500 font-mono truncate"> <div className="text-xs text-slate-500 font-mono truncate">
{status?.connection_type?.toUpperCase()}: {status?.connection_target} {status?.connection_type?.toUpperCase()}: {status?.connection_target}
</div> </div>
<div className="text-xs text-slate-500 mt-1"> <div className="text-xs text-slate-500 mt-1">
Uptime: {status ? formatUptime(status.uptime_seconds) : '...'} Uptime: {status ? formatUptime(status.uptime_seconds) : '...'}
</div> </div>
</div> </div>
</aside> </aside>
{/* Main content */} {/* Main content */}
<div className="ml-[220px] flex-1 flex flex-col"> <div className="flex-1 flex flex-col overflow-hidden">
{/* Header */} {/* Header */}
<header className="h-14 border-b border-border bg-bg-card flex items-center justify-between px-6"> <header className="h-14 flex-shrink-0 border-b border-border bg-bg-card flex items-center justify-between px-6">
<h1 className="text-lg font-semibold"> <h1 className="text-lg font-semibold">
{getPageTitle(location.pathname)} {getPageTitle(location.pathname)}
</h1> </h1>
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
{/* Live indicator */} {/* Live indicator */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div <div
className={`w-2 h-2 rounded-full ${ className={`w-2 h-2 rounded-full ${
connected ? 'bg-green-500 animate-pulse-slow' : 'bg-slate-500' connected ? 'bg-green-500 animate-pulse-slow' : 'bg-slate-500'
}`} }`}
/> />
<span className="text-xs text-slate-400"> <span className="text-xs text-slate-400">
{connected ? 'Live' : 'Offline'} {connected ? 'Live' : 'Offline'}
</span> </span>
</div> </div>
{/* Clock */} {/* Clock */}
<div className="text-sm font-mono text-slate-400"> <div className="text-sm font-mono text-slate-400">
{timeStr} MT {timeStr} MT
</div> </div>
</div> </div>
</header> </header>
{/* Page content */} {/* Page content */}
<main className="flex-1 p-6 overflow-auto">{children}</main> <main className="flex-1 overflow-y-auto p-6">{children}</main>
</div> </div>
</div> </div>
) )
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,20 +1,17 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="en" class="dark">
<head> <head>
<title>MeshAI Dashboard</title> <meta charset="UTF-8" />
<meta charset="utf-8"> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head> <title>MeshAI Dashboard</title>
<body style="background:#0a0e17;color:#e2e8f0;font-family:system-ui,-apple-system,sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0"> <link rel="preconnect" href="https://fonts.googleapis.com">
<div style="text-align:center;max-width:600px;padding:2rem"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<h1 style="font-size:2.5rem;margin-bottom:1rem;font-weight:600">MeshAI Dashboard</h1> <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<p style="color:#64748b;margin-bottom:2rem">Frontend build pending - API is live.</p> <script type="module" crossorigin src="/assets/index-DnO02g6m.js"></script>
<div style="display:flex;gap:1rem;justify-content:center;flex-wrap:wrap"> <link rel="stylesheet" crossorigin href="/assets/index-DdqEB3wX.css">
<a href="/api/status" style="color:#60a5fa;text-decoration:none;padding:0.5rem 1rem;border:1px solid #334155;border-radius:0.5rem">/api/status</a> </head>
<a href="/api/health" style="color:#60a5fa;text-decoration:none;padding:0.5rem 1rem;border:1px solid #334155;border-radius:0.5rem">/api/health</a> <body>
<a href="/api/nodes" style="color:#60a5fa;text-decoration:none;padding:0.5rem 1rem;border:1px solid #334155;border-radius:0.5rem">/api/nodes</a> <div id="root"></div>
<a href="/api/config" style="color:#60a5fa;text-decoration:none;padding:0.5rem 1rem;border:1px solid #334155;border-radius:0.5rem">/api/config</a> </body>
</div> </html>
</div>
</body>
</html>