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:
2026-02-10 14:56:14 -08:00
parent 6ad79bde75
commit fcd8a5e08c
5 changed files with 231 additions and 32 deletions
@@ -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>
)
}
+2 -2
View File
@@ -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
View File
@@ -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 &mdash; z@lux.network / admin
</p>
</div>
</CardContent>
+10 -10
View File
@@ -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
View File
@@ -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"
}, [])