mirror of
https://github.com/hanzoai/bootnode.git
synced 2026-09-30 20:46:32 +00:00
fix: dark theme, local auth redirect, and monitoring page
- Dashboard and navigation use dark-mode-aware CSS vars (bg-background, bg-sidebar, border-border) instead of hardcoded bg-gray-50/bg-white - isProductionMode() now recognizes 127.0.0.1 and private IPs as local so login no longer redirects to lux.id on localhost - Add /dashboard/infrastructure/monitoring page with node health summary cards, CPU/memory/disk usage bars, 15s auto-refresh - Quick login button uses [email protected] / admin credentials
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Activity, Cpu, HardDrive, MemoryStick, Wifi, WifiOff, Loader2 } from "lucide-react"
|
||||
|
||||
interface NodeHealth {
|
||||
id: string
|
||||
name: string
|
||||
chain: string
|
||||
status: "healthy" | "degraded" | "down"
|
||||
cpu_percent: number
|
||||
memory_percent: number
|
||||
disk_percent: number
|
||||
block_height: number
|
||||
peers: number
|
||||
uptime_seconds: number
|
||||
last_check: string
|
||||
}
|
||||
|
||||
interface MonitoringStats {
|
||||
total_nodes: number
|
||||
healthy: number
|
||||
degraded: number
|
||||
down: number
|
||||
nodes: NodeHealth[]
|
||||
}
|
||||
|
||||
function formatUptime(seconds: number): string {
|
||||
const d = Math.floor(seconds / 86400)
|
||||
const h = Math.floor((seconds % 86400) / 3600)
|
||||
if (d > 0) return `${d}d ${h}h`
|
||||
const m = Math.floor((seconds % 3600) / 60)
|
||||
return `${h}h ${m}m`
|
||||
}
|
||||
|
||||
function StatusBadge({ status }: { status: string }) {
|
||||
const variant = status === "healthy" ? "default" : status === "degraded" ? "secondary" : "destructive"
|
||||
return <Badge variant={variant}>{status}</Badge>
|
||||
}
|
||||
|
||||
function UsageBar({ percent, label }: { percent: number; label: string }) {
|
||||
const color = percent > 90 ? "bg-destructive" : percent > 70 ? "bg-yellow-500" : "bg-green-500"
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-xs">
|
||||
<span className="text-muted-foreground">{label}</span>
|
||||
<span>{percent.toFixed(0)}%</span>
|
||||
</div>
|
||||
<div className="h-1.5 rounded-full bg-muted">
|
||||
<div className={`h-full rounded-full ${color}`} style={{ width: `${Math.min(percent, 100)}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function MonitoringPage() {
|
||||
const [stats, setStats] = useState<MonitoringStats | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
function getAuthHeaders(): Record<string, string> {
|
||||
const token = localStorage.getItem("bootnode_token")
|
||||
const apiKey = localStorage.getItem("bootnode_api_key")
|
||||
if (token) return { Authorization: `Bearer ${token}` }
|
||||
if (apiKey) return { "X-API-Key": apiKey }
|
||||
return {}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchMonitoring() {
|
||||
try {
|
||||
const res = await fetch("http://localhost:8000/v1/monitoring/nodes", {
|
||||
headers: getAuthHeaders(),
|
||||
})
|
||||
if (res.ok) {
|
||||
setStats(await res.json())
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
fetchMonitoring()
|
||||
const interval = setInterval(fetchMonitoring, 15000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Monitoring</h1>
|
||||
<p className="text-muted-foreground">Real-time health and performance of your infrastructure</p>
|
||||
</div>
|
||||
|
||||
{/* Summary Cards */}
|
||||
<div className="grid gap-4 md:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Nodes</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
{loading ? <Loader2 className="h-6 w-6 animate-spin" /> : stats?.total_nodes ?? 0}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||
<Wifi className="h-4 w-4 text-green-500" /> Healthy
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-green-500">
|
||||
{loading ? <Loader2 className="h-6 w-6 animate-spin" /> : stats?.healthy ?? 0}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||
<Activity className="h-4 w-4 text-yellow-500" /> Degraded
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-yellow-500">
|
||||
{loading ? <Loader2 className="h-6 w-6 animate-spin" /> : stats?.degraded ?? 0}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||
<WifiOff className="h-4 w-4 text-destructive" /> Down
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-destructive">
|
||||
{loading ? <Loader2 className="h-6 w-6 animate-spin" /> : stats?.down ?? 0}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Node Health Table */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Node Health</CardTitle>
|
||||
<CardDescription>Auto-refreshes every 15 seconds</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : !stats?.nodes?.length ? (
|
||||
<div className="text-center py-12 text-muted-foreground">
|
||||
<Activity className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
||||
<p>No nodes to monitor yet.</p>
|
||||
<p className="text-sm mt-1">Deploy nodes from the Infrastructure page to see monitoring data.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{stats.nodes.map((node) => (
|
||||
<div key={node.id} className="border rounded-lg p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-medium">{node.name}</span>
|
||||
<Badge variant="outline">{node.chain}</Badge>
|
||||
<StatusBadge status={node.status} />
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<span>Block #{node.block_height.toLocaleString()}</span>
|
||||
<span>{node.peers} peers</span>
|
||||
<span>Up {formatUptime(node.uptime_seconds)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<UsageBar percent={node.cpu_percent} label="CPU" />
|
||||
<UsageBar percent={node.memory_percent} label="Memory" />
|
||||
<UsageBar percent={node.disk_percent} label="Disk" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -13,9 +13,9 @@ interface DashboardLayoutProps {
|
||||
export default function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
return (
|
||||
<ProtectedRoute>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<UnifiedNavigation />
|
||||
|
||||
|
||||
{/* Main Content Area */}
|
||||
<main className="pl-64 pt-16">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
|
||||
+19
-16
@@ -179,26 +179,29 @@ function LoginForm() {
|
||||
</form>
|
||||
</Tabs>
|
||||
|
||||
<p className="mt-6 text-center text-xs text-muted-foreground">
|
||||
Development mode - local authentication
|
||||
</p>
|
||||
|
||||
{/* Demo credentials for localhost */}
|
||||
<div className="mt-3 rounded-md border border-dashed border-muted-foreground/25 bg-muted/50 p-3">
|
||||
{/* Quick login for local dev */}
|
||||
<div className="mt-6 space-y-2">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<span className="w-full border-t" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-card px-2 text-muted-foreground">Quick Login</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full text-xs h-8"
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
disabled={loading}
|
||||
onClick={async () => {
|
||||
setMode("login")
|
||||
setEmail("[email protected]")
|
||||
setPassword("testpass123")
|
||||
setEmail("[email protected]")
|
||||
setPassword("admin")
|
||||
setLoading(true)
|
||||
setError("")
|
||||
try {
|
||||
await login("[email protected]", "testpass123")
|
||||
await login("[email protected]", "admin")
|
||||
const returnUrl = searchParams.get("returnUrl") || "/dashboard"
|
||||
router.push(returnUrl)
|
||||
} catch (err) {
|
||||
@@ -210,15 +213,15 @@ function LoginForm() {
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-3 w-3 animate-spin" />
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Signing in...
|
||||
</>
|
||||
) : (
|
||||
"Use Demo Account"
|
||||
<>Sign in as Admin (z@lux.network)</>
|
||||
)}
|
||||
</Button>
|
||||
<p className="text-[10px] text-muted-foreground mt-2 text-center">
|
||||
test@hanzo.ai / testpass123
|
||||
<p className="text-center text-[10px] text-muted-foreground">
|
||||
Local dev only — z@lux.network / admin
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -162,7 +162,7 @@ export function UnifiedNavigation() {
|
||||
return (
|
||||
<>
|
||||
{/* Top Navigation Bar */}
|
||||
<nav className="bg-white border-b border-gray-200 fixed w-full top-0 z-50">
|
||||
<nav className="bg-background border-b border-border fixed w-full top-0 z-50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between h-16">
|
||||
<div className="flex items-center">
|
||||
@@ -267,7 +267,7 @@ export function UnifiedNavigation() {
|
||||
</nav>
|
||||
|
||||
{/* Side Navigation */}
|
||||
<div className="fixed inset-y-0 left-0 z-40 w-64 bg-gray-50 border-r border-gray-200 pt-16 overflow-y-auto">
|
||||
<div className="fixed inset-y-0 left-0 z-40 w-64 bg-sidebar border-r border-border pt-16 overflow-y-auto">
|
||||
<nav className="p-4 space-y-2">
|
||||
{/* Main Navigation */}
|
||||
{navigation.map((item) => (
|
||||
@@ -277,8 +277,8 @@ export function UnifiedNavigation() {
|
||||
{/* Admin Navigation (if admin) */}
|
||||
{isAdmin && (
|
||||
<>
|
||||
<div className="pt-4 mt-4 border-t border-gray-200">
|
||||
<h3 className="px-3 text-xs font-semibold text-gray-500 uppercase tracking-wider">
|
||||
<div className="pt-4 mt-4 border-t border-border">
|
||||
<h3 className="px-3 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
||||
Administration
|
||||
</h3>
|
||||
</div>
|
||||
@@ -290,7 +290,7 @@ export function UnifiedNavigation() {
|
||||
</nav>
|
||||
|
||||
{/* Organization Info */}
|
||||
<div className="absolute bottom-0 left-0 right-0 p-4 bg-white border-t">
|
||||
<div className="absolute bottom-0 left-0 right-0 p-4 bg-background border-t border-border">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div
|
||||
className="w-8 h-8 rounded-lg flex items-center justify-center text-white text-sm font-bold"
|
||||
@@ -310,7 +310,7 @@ export function UnifiedNavigation() {
|
||||
{mobileMenuOpen && (
|
||||
<div className="fixed inset-0 z-30 md:hidden">
|
||||
<div className="absolute inset-0 bg-black opacity-25" onClick={() => setMobileMenuOpen(false)} />
|
||||
<div className="absolute left-0 top-16 w-64 h-full bg-white border-r border-gray-200 overflow-y-auto">
|
||||
<div className="absolute left-0 top-16 w-64 h-full bg-background border-r border-border overflow-y-auto">
|
||||
<nav className="p-4 space-y-2">
|
||||
{navigation.concat(isAdmin ? adminNavigation : []).map((item) => (
|
||||
<NavItem
|
||||
@@ -353,9 +353,9 @@ function NavItem({
|
||||
onClick={onClick}
|
||||
className={`
|
||||
flex items-center justify-between w-full px-3 py-2 text-sm font-medium rounded-md transition-colors
|
||||
${isActive(item.href)
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'text-gray-700 hover:bg-gray-100'
|
||||
${isActive(item.href)
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'text-foreground/80 hover:bg-accent'
|
||||
}
|
||||
`}
|
||||
>
|
||||
@@ -391,7 +391,7 @@ function NavItem({
|
||||
flex items-center space-x-2 px-3 py-2 text-sm rounded-md transition-colors
|
||||
${isActive(child.href)
|
||||
? 'bg-primary/10 text-primary'
|
||||
: 'text-gray-600 hover:bg-gray-100'
|
||||
: 'text-muted-foreground hover:bg-accent'
|
||||
}
|
||||
`}
|
||||
>
|
||||
|
||||
+7
-4
@@ -38,8 +38,9 @@ const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
||||
// Check if we're in production mode
|
||||
const isProductionMode = () => {
|
||||
if (typeof window === "undefined") return false
|
||||
return process.env.NEXT_PUBLIC_AUTH_MODE === "iam" ||
|
||||
window.location.hostname !== "localhost"
|
||||
if (process.env.NEXT_PUBLIC_AUTH_MODE === "iam") return true
|
||||
const h = window.location.hostname
|
||||
return h !== "localhost" && h !== "127.0.0.1" && h !== "0.0.0.0" && !h.startsWith("192.168.") && !h.startsWith("10.")
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
@@ -60,8 +61,10 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
||||
const isProduction = isProductionMode()
|
||||
|
||||
const getApiUrl = useCallback(() => {
|
||||
if (typeof window !== "undefined" && window.location.hostname !== "localhost") {
|
||||
return getBrand().apiUrl
|
||||
if (typeof window !== "undefined") {
|
||||
const h = window.location.hostname
|
||||
const isLocal = h === "localhost" || h === "127.0.0.1" || h === "0.0.0.0" || h.startsWith("192.168.") || h.startsWith("10.")
|
||||
if (!isLocal) return getBrand().apiUrl
|
||||
}
|
||||
return process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"
|
||||
}, [])
|
||||
|
||||
Reference in New Issue
Block a user