mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-05-21 23:24:44 +02:00
fix(dashboard): content scroll overflow bug
This commit is contained in:
parent
4331bcb7e1
commit
374fb835c5
4 changed files with 327 additions and 182 deletions
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
meshai/dashboard/static/assets/index-DdqEB3wX.css
Normal file
1
meshai/dashboard/static/assets/index-DdqEB3wX.css
Normal file
File diff suppressed because one or more lines are too long
147
meshai/dashboard/static/assets/index-DnO02g6m.js
Normal file
147
meshai/dashboard/static/assets/index-DnO02g6m.js
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -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>
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue