working on storage dashboard

This commit is contained in:
Warp Agent
2025-12-25 09:01:49 +00:00
parent a08514b4f2
commit a5e6197bca
29 changed files with 4028 additions and 528 deletions

View File

@@ -1,10 +1,15 @@
<!doctype html>
<html lang="en">
<html class="dark" lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AtlasOS - Calypso</title>
<!-- Material Symbols -->
<link
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap"
rel="stylesheet"
/>
</head>
<body>
<div id="root"></div>

View File

@@ -32,7 +32,10 @@ export interface Metrics {
system: {
cpu_usage_percent: number
memory_usage_percent: number
memory_used_bytes: number
memory_total_bytes: number
disk_usage_percent: number
uptime_seconds: number
}
storage: {
total_repositories: number

View File

@@ -11,6 +11,7 @@ export interface PhysicalDisk {
is_ssd: boolean
health_status: string
is_used: boolean
attached_to_pool?: string // Pool name if disk is used in a ZFS pool
created_at: string
updated_at: string
}
@@ -44,8 +45,8 @@ export interface Repository {
export const storageApi = {
listDisks: async (): Promise<PhysicalDisk[]> => {
const response = await apiClient.get<PhysicalDisk[]>('/storage/disks')
return response.data
const response = await apiClient.get<{ disks: PhysicalDisk[] | null }>('/storage/disks')
return response.data.disks || []
},
syncDisks: async (): Promise<{ task_id: string }> => {
@@ -54,13 +55,13 @@ export const storageApi = {
},
listVolumeGroups: async (): Promise<VolumeGroup[]> => {
const response = await apiClient.get<VolumeGroup[]>('/storage/volume-groups')
return response.data
const response = await apiClient.get<{ volume_groups: VolumeGroup[] | null }>('/storage/volume-groups')
return response.data.volume_groups || []
},
listRepositories: async (): Promise<Repository[]> => {
const response = await apiClient.get<Repository[]>('/storage/repositories')
return response.data
const response = await apiClient.get<{ repositories: Repository[] | null }>('/storage/repositories')
return response.data.repositories || []
},
getRepository: async (id: string): Promise<Repository> => {
@@ -84,3 +85,92 @@ export const storageApi = {
},
}
export interface ZFSPool {
id: string
name: string
description?: string
raid_level: string // stripe, mirror, raidz, raidz2, raidz3
disks: string[] // device paths
spare_disks?: string[] // spare disk paths
size_bytes: number
used_bytes: number
compression: string // off, lz4, zstd, gzip
deduplication: boolean
auto_expand: boolean
scrub_interval: number // days
is_active: boolean
health_status: string // online, degraded, faulted, offline
created_at: string
updated_at: string
created_by: string
}
export const zfsApi = {
listPools: async (): Promise<ZFSPool[]> => {
const response = await apiClient.get<{ pools: ZFSPool[] | null }>('/storage/zfs/pools')
return response.data.pools || []
},
getPool: async (id: string): Promise<ZFSPool> => {
const response = await apiClient.get<ZFSPool>(`/storage/zfs/pools/${id}`)
return response.data
},
createPool: async (data: {
name: string
description?: string
raid_level: string
disks: string[]
compression?: string
deduplication?: boolean
auto_expand?: boolean
}): Promise<ZFSPool> => {
const response = await apiClient.post<ZFSPool>('/storage/zfs/pools', data)
return response.data
},
deletePool: async (id: string): Promise<void> => {
await apiClient.delete(`/storage/zfs/pools/${id}`)
},
addSpareDisk: async (id: string, disks: string[]): Promise<void> => {
await apiClient.post(`/storage/zfs/pools/${id}/spare`, { disks })
},
listDatasets: async (poolId: string): Promise<ZFSDataset[]> => {
const response = await apiClient.get<{ datasets: ZFSDataset[] | null }>(`/storage/zfs/pools/${poolId}/datasets`)
return response.data.datasets || []
},
createDataset: async (poolId: string, data: {
name: string
type: 'filesystem' | 'volume'
compression?: string
quota?: number
reservation?: number
mount_point?: string
}): Promise<ZFSDataset> => {
const response = await apiClient.post<ZFSDataset>(`/storage/zfs/pools/${poolId}/datasets`, data)
return response.data
},
deleteDataset: async (poolId: string, datasetName: string): Promise<void> => {
await apiClient.delete(`/storage/zfs/pools/${poolId}/datasets/${datasetName}`)
},
}
export interface ZFSDataset {
name: string
pool: string
type: string // filesystem, volume, snapshot
mount_point: string
used_bytes: number
available_bytes: number
referenced_bytes: number
compression: string
deduplication: string
quota: number // -1 for unlimited
reservation: number
created_at: string
}

24
frontend/src/api/tasks.ts Normal file
View File

@@ -0,0 +1,24 @@
import apiClient from './client'
export interface Task {
id: string
type: string
status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'
progress: number
message: string
error_message?: string
created_by?: string
started_at?: string
completed_at?: string
created_at: string
updated_at: string
metadata?: Record<string, any>
}
export const tasksApi = {
getTask: async (id: string): Promise<Task> => {
const response = await apiClient.get<Task>(`/tasks/${id}`)
return response.data
},
}

View File

@@ -1,11 +1,23 @@
import { Outlet, Link, useNavigate } from 'react-router-dom'
import { Outlet, Link, useNavigate, useLocation } from 'react-router-dom'
import { useAuthStore } from '@/store/auth'
import { LogOut, Menu } from 'lucide-react'
import {
LogOut,
Menu,
LayoutDashboard,
HardDrive,
Database,
Network,
Settings,
Bell,
Server,
Users
} from 'lucide-react'
import { useState } from 'react'
export default function Layout() {
const { user, clearAuth } = useAuthStore()
const navigate = useNavigate()
const location = useLocation()
const [sidebarOpen, setSidebarOpen] = useState(true)
const handleLogout = () => {
@@ -14,84 +26,100 @@ export default function Layout() {
}
const navigation = [
{ name: 'Dashboard', href: '/', icon: '📊' },
{ name: 'Storage', href: '/storage', icon: '💾' },
{ name: 'Tape Libraries', href: '/tape', icon: '📼' },
{ name: 'iSCSI Targets', href: '/iscsi', icon: '🔌' },
{ name: 'Tasks', href: '/tasks', icon: '⚙️' },
{ name: 'Alerts', href: '/alerts', icon: '🔔' },
{ name: 'System', href: '/system', icon: '🖥️' },
{ name: 'Dashboard', href: '/', icon: LayoutDashboard },
{ name: 'Storage', href: '/storage', icon: HardDrive },
{ name: 'Tape Libraries', href: '/tape', icon: Database },
{ name: 'iSCSI Targets', href: '/iscsi', icon: Network },
{ name: 'Tasks', href: '/tasks', icon: Settings },
{ name: 'Alerts', href: '/alerts', icon: Bell },
{ name: 'System', href: '/system', icon: Server },
]
if (user?.roles.includes('admin')) {
navigation.push({ name: 'IAM', href: '/iam', icon: '👥' })
navigation.push({ name: 'IAM', href: '/iam', icon: Users })
}
const isActive = (href: string) => {
if (href === '/') {
return location.pathname === '/'
}
return location.pathname.startsWith(href)
}
return (
<div className="min-h-screen bg-gray-50">
<div className="min-h-screen bg-background-dark">
{/* Sidebar */}
<div
className={`fixed inset-y-0 left-0 z-50 w-64 bg-gray-900 text-white transition-transform duration-300 ${
className={`fixed inset-y-0 left-0 z-50 w-64 bg-background-dark border-r border-border-dark text-white transition-transform duration-300 ${
sidebarOpen ? 'translate-x-0' : '-translate-x-full'
}`}
>
<div className="flex flex-col h-full">
<div className="flex items-center justify-between p-4 border-b border-gray-800">
<h1 className="text-xl font-bold">Calypso</h1>
{/* Header */}
<div className="flex items-center justify-between px-6 py-5 border-b border-border-dark">
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
<span className="text-white font-bold text-sm">C</span>
</div>
<h1 className="text-xl font-black text-white font-display tracking-tight">Calypso</h1>
</div>
<button
onClick={() => setSidebarOpen(false)}
className="lg:hidden text-gray-400 hover:text-white"
className="lg:hidden text-text-secondary hover:text-white transition-colors"
>
<Menu className="h-6 w-6" />
<Menu className="h-5 w-5" />
</button>
</div>
<nav className="flex-1 p-4 space-y-2">
{navigation.map((item) => (
<Link
key={item.name}
to={item.href}
className="flex items-center space-x-3 px-4 py-2 rounded-lg hover:bg-gray-800 transition-colors"
>
<span>{item.icon}</span>
<span>{item.name}</span>
</Link>
))}
{/* Navigation */}
<nav className="flex-1 px-3 py-4 space-y-1 overflow-y-auto custom-scrollbar">
{navigation.map((item) => {
const Icon = item.icon
const active = isActive(item.href)
return (
<Link
key={item.name}
to={item.href}
className={`flex items-center gap-3 px-4 py-2.5 rounded-lg transition-all ${
active
? 'bg-primary/20 text-primary border-l-2 border-primary'
: 'text-text-secondary hover:bg-card-dark hover:text-white'
}`}
>
<Icon className={`h-5 w-5 ${active ? 'text-primary' : ''}`} />
<span className={`text-sm font-medium ${active ? 'font-semibold' : ''}`}>
{item.name}
</span>
</Link>
)
})}
</nav>
<div className="p-4 border-t border-gray-800">
<div className="flex items-center justify-between mb-4">
<div>
<p className="text-sm font-medium">{user?.username}</p>
<p className="text-xs text-gray-400">{user?.roles.join(', ')}</p>
</div>
{/* Footer */}
<div className="p-4 border-t border-border-dark bg-[#0d1419]">
<div className="mb-3 px-2">
<p className="text-sm font-semibold text-white mb-0.5">{user?.username}</p>
<p className="text-xs text-text-secondary font-mono">
{user?.roles.join(', ').toUpperCase()}
</p>
</div>
<button
onClick={handleLogout}
className="w-full flex items-center space-x-2 px-4 py-2 rounded-lg hover:bg-gray-800 transition-colors"
className="w-full flex items-center gap-2 px-4 py-2.5 rounded-lg text-text-secondary hover:bg-card-dark hover:text-white transition-colors border border-border-dark"
>
<LogOut className="h-4 w-4" />
<span>Logout</span>
<span className="text-sm font-medium">Logout</span>
</button>
</div>
</div>
</div>
{/* Main content */}
<div className={`transition-all duration-300 ${sidebarOpen ? 'lg:ml-64' : 'ml-0'}`}>
{/* Top bar */}
<div className="bg-white shadow-sm border-b border-gray-200">
<div className="flex items-center justify-between px-6 py-4">
<button
onClick={() => setSidebarOpen(!sidebarOpen)}
className="lg:hidden text-gray-600 hover:text-gray-900"
>
<Menu className="h-6 w-6" />
</button>
<div className="flex-1" />
</div>
</div>
<div className={`transition-all duration-300 ${sidebarOpen ? 'lg:ml-64' : 'ml-0'} bg-background-dark`}>
{/* Top bar - removed for dashboard design */}
{/* Page content */}
<main className="p-6">
<main className="min-h-screen">
<Outlet />
</main>
</div>

View File

@@ -16,7 +16,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
{
"bg-primary text-primary-foreground hover:bg-primary/90": variant === "default",
"bg-destructive text-destructive-foreground hover:bg-destructive/90": variant === "destructive",
"border border-input bg-background hover:bg-accent hover:text-accent-foreground": variant === "outline",
"border border-border-dark bg-card-dark hover:bg-[#233648] hover:text-white text-white": variant === "outline",
"bg-secondary text-secondary-foreground hover:bg-secondary/80": variant === "secondary",
"hover:bg-accent hover:text-accent-foreground": variant === "ghost",
"text-primary underline-offset-4 hover:underline": variant === "link",

View File

@@ -8,7 +8,7 @@ const Card = React.forwardRef<
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
"rounded-lg border border-border-dark bg-card-dark text-white shadow-sm",
className
)}
{...props}
@@ -49,7 +49,7 @@ const CardDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
className={cn("text-sm text-text-secondary", className)}
{...props}
/>
))

View File

@@ -1,3 +1,5 @@
@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@200..800&family=JetBrains+Mono:wght@400;500&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
@@ -55,6 +57,68 @@
}
body {
@apply bg-background text-foreground;
font-family: 'Manrope', sans-serif;
}
}
/* Custom scrollbar for dark theme */
.custom-scrollbar::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #111a22;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #324d67;
border-radius: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #476685;
}
/* Electric glow animation for buttons */
@keyframes electric-glow {
0%, 100% {
box-shadow: 0 0 8px rgba(19, 127, 236, 0.4),
0 0 16px rgba(19, 127, 236, 0.3),
0 0 24px rgba(19, 127, 236, 0.2),
inset 0 0 8px rgba(19, 127, 236, 0.1);
}
50% {
box-shadow: 0 0 12px rgba(19, 127, 236, 0.6),
0 0 24px rgba(19, 127, 236, 0.4),
0 0 36px rgba(19, 127, 236, 0.3),
inset 0 0 12px rgba(19, 127, 236, 0.15);
}
}
@keyframes electric-border {
0%, 100% {
border-color: rgba(19, 127, 236, 0.3);
}
50% {
border-color: rgba(19, 127, 236, 0.6);
}
}
.electric-glow {
animation: electric-glow 2.5s ease-in-out infinite;
}
.electric-glow-border {
animation: electric-border 2.5s ease-in-out infinite;
}
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}

View File

@@ -13,9 +13,9 @@ const severityIcons = {
}
const severityColors = {
info: 'bg-blue-100 text-blue-800 border-blue-200',
warning: 'bg-yellow-100 text-yellow-800 border-yellow-200',
critical: 'bg-red-100 text-red-800 border-red-200',
info: 'bg-blue-500/20 text-blue-400 border-blue-500/30',
warning: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30',
critical: 'bg-red-500/20 text-red-400 border-red-500/30',
}
export default function AlertsPage() {
@@ -47,11 +47,11 @@ export default function AlertsPage() {
const alerts = data?.alerts || []
return (
<div className="space-y-6">
<div className="space-y-6 min-h-screen bg-background-dark p-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">Alerts</h1>
<p className="mt-2 text-sm text-gray-600">
<h1 className="text-3xl font-bold text-white">Alerts</h1>
<p className="mt-2 text-sm text-text-secondary">
Monitor system alerts and notifications
</p>
</div>
@@ -85,7 +85,7 @@ export default function AlertsPage() {
</CardHeader>
<CardContent>
{isLoading ? (
<p className="text-sm text-gray-500">Loading alerts...</p>
<p className="text-sm text-text-secondary">Loading alerts...</p>
) : alerts.length > 0 ? (
<div className="space-y-4">
{alerts.map((alert: Alert) => {
@@ -93,19 +93,19 @@ export default function AlertsPage() {
return (
<div
key={alert.id}
className={`border rounded-lg p-4 ${severityColors[alert.severity]}`}
className={`border rounded-lg p-4 bg-card-dark ${severityColors[alert.severity]}`}
>
<div className="flex items-start justify-between">
<div className="flex items-start space-x-3 flex-1">
<Icon className="h-5 w-5 mt-0.5" />
<div className="flex-1">
<div className="flex items-center space-x-2 mb-1">
<h3 className="font-semibold">{alert.title}</h3>
<span className="text-xs px-2 py-1 bg-white/50 rounded">
<h3 className="font-semibold text-white">{alert.title}</h3>
<span className="text-xs px-2 py-1 bg-[#233648] border border-border-dark rounded text-text-secondary">
{alert.source}
</span>
</div>
<p className="text-sm mb-2">{alert.message}</p>
<p className="text-sm mb-2 text-text-secondary">{alert.message}</p>
<div className="flex items-center space-x-4 text-xs">
<span>{formatRelativeTime(alert.created_at)}</span>
{alert.resource_type && (
@@ -147,8 +147,8 @@ export default function AlertsPage() {
</div>
) : (
<div className="text-center py-8">
<Bell className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<p className="text-sm text-gray-500">No alerts found</p>
<Bell className="h-12 w-12 text-text-secondary mx-auto mb-4" />
<p className="text-sm text-text-secondary">No alerts found</p>
</div>
)}
</CardContent>

View File

@@ -1,190 +1,609 @@
import { useQuery } from '@tanstack/react-query'
import { Link } from 'react-router-dom'
import { useState, useMemo, useEffect } from 'react'
import apiClient from '@/api/client'
import { monitoringApi } from '@/api/monitoring'
import { storageApi } from '@/api/storage'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Activity, Database, AlertTriangle, HardDrive } from 'lucide-react'
import { formatBytes } from '@/lib/format'
import {
Cpu,
MemoryStick,
Clock,
Activity,
RefreshCw,
TrendingDown,
CheckCircle2,
AlertTriangle
} from 'lucide-react'
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
} from 'recharts'
export default function Dashboard() {
const [activeTab, setActiveTab] = useState<'jobs' | 'logs' | 'alerts'>('jobs')
const [networkDataPoints, setNetworkDataPoints] = useState<Array<{ time: string; inbound: number; outbound: number }>>([])
const refreshInterval = 5
const { data: health } = useQuery({
queryKey: ['health'],
queryFn: async () => {
const response = await apiClient.get('/health')
return response.data
},
refetchInterval: refreshInterval * 1000,
})
const { data: metrics } = useQuery({
queryKey: ['metrics'],
queryFn: monitoringApi.getMetrics,
refetchInterval: refreshInterval * 1000,
})
const { data: alerts } = useQuery({
queryKey: ['alerts', 'dashboard'],
queryFn: () => monitoringApi.listAlerts({ is_acknowledged: false, limit: 5 }),
queryFn: () => monitoringApi.listAlerts({ is_acknowledged: false, limit: 10 }),
refetchInterval: refreshInterval * 1000,
})
const { data: repositories } = useQuery({
const { data: repositories = [] } = useQuery({
queryKey: ['storage', 'repositories'],
queryFn: storageApi.listRepositories,
})
const unacknowledgedAlerts = alerts?.alerts?.length || 0
const totalRepos = repositories?.length || 0
const totalStorage = repositories?.reduce((sum, repo) => sum + repo.size_bytes, 0) || 0
const usedStorage = repositories?.reduce((sum, repo) => sum + repo.used_bytes, 0) || 0
// Calculate uptime (mock for now, would come from metrics)
const uptime = metrics?.system?.uptime_seconds || 0
const days = Math.floor(uptime / 86400)
const hours = Math.floor((uptime % 86400) / 3600)
const minutes = Math.floor((uptime % 3600) / 60)
// Mock active jobs (would come from tasks API)
const activeJobs = [
{
id: '1',
name: 'Daily Backup: VM-Cluster-01',
type: 'Replication',
progress: 45,
speed: '145 MB/s',
status: 'running',
eta: '1h 12m',
},
{
id: '2',
name: 'ZFS Scrub: Pool-01',
type: 'Maintenance',
progress: 78,
speed: '1.2 GB/s',
status: 'running',
},
]
// Mock system logs
const systemLogs = [
{ time: '10:45:22', level: 'INFO', source: 'systemd', message: 'Started User Manager for UID 1000.' },
{ time: '10:45:15', level: 'WARN', source: 'smartd', message: 'Device: /dev/ada5, SMART Usage Attribute: 194 Temperature_Celsius changed from 38 to 41' },
{ time: '10:44:58', level: 'INFO', source: 'kernel', message: 'ix0: link state changed to UP' },
{ time: '10:42:10', level: 'INFO', source: 'zfs', message: 'zfs_arc_reclaim_thread: reclaiming 157286400 bytes ...' },
]
const totalStorage = Array.isArray(repositories) ? repositories.reduce((sum, repo) => sum + (repo?.size_bytes || 0), 0) : 0
const usedStorage = Array.isArray(repositories) ? repositories.reduce((sum, repo) => sum + (repo?.used_bytes || 0), 0) : 0
const storagePercent = totalStorage > 0 ? (usedStorage / totalStorage) * 100 : 0
// Initialize network data
useEffect(() => {
// Generate initial 30 data points
const initialData = []
const now = Date.now()
for (let i = 29; i >= 0; i--) {
const time = new Date(now - i * 5000)
const minutes = time.getMinutes().toString().padStart(2, '0')
const seconds = time.getSeconds().toString().padStart(2, '0')
const baseInbound = 800 + Math.random() * 400
const baseOutbound = 400 + Math.random() * 200
initialData.push({
time: `${minutes}:${seconds}`,
inbound: Math.round(baseInbound),
outbound: Math.round(baseOutbound),
})
}
setNetworkDataPoints(initialData)
// Update data every 5 seconds
const interval = setInterval(() => {
setNetworkDataPoints((prev) => {
const now = new Date()
const minutes = now.getMinutes().toString().padStart(2, '0')
const seconds = now.getSeconds().toString().padStart(2, '0')
const baseInbound = 800 + Math.random() * 400
const baseOutbound = 400 + Math.random() * 200
const newPoint = {
time: `${minutes}:${seconds}`,
inbound: Math.round(baseInbound),
outbound: Math.round(baseOutbound),
}
// Keep only last 30 points
const updated = [...prev.slice(1), newPoint]
return updated
})
}, 5000)
return () => clearInterval(interval)
}, [])
// Calculate current and peak throughput
const currentThroughput = useMemo(() => {
if (networkDataPoints.length === 0) return { inbound: 0, outbound: 0, total: 0 }
const last = networkDataPoints[networkDataPoints.length - 1]
return {
inbound: last.inbound,
outbound: last.outbound,
total: last.inbound + last.outbound,
}
}, [networkDataPoints])
const peakThroughput = useMemo(() => {
if (networkDataPoints.length === 0) return 0
return Math.max(...networkDataPoints.map((d) => d.inbound + d.outbound))
}, [networkDataPoints])
const systemStatus = health?.status === 'healthy' ? 'System Healthy' : 'System Degraded'
const isHealthy = health?.status === 'healthy'
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold text-gray-900">Dashboard</h1>
<p className="mt-2 text-sm text-gray-600">
Overview of your Calypso backup appliance
</p>
</div>
{/* System Health */}
<Card>
<CardHeader>
<CardTitle className="flex items-center">
<Activity className="h-5 w-5 mr-2" />
System Health
</CardTitle>
</CardHeader>
<CardContent>
{health && (
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2">
<span
className={`h-4 w-4 rounded-full ${
health.status === 'healthy'
? 'bg-green-500'
: health.status === 'degraded'
? 'bg-yellow-500'
: 'bg-red-500'
}`}
/>
<span className="text-lg font-semibold capitalize">{health.status}</span>
</div>
<span className="text-sm text-gray-600">Service: {health.service}</span>
</div>
)}
</CardContent>
</Card>
{/* Overview Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Storage Repositories</CardTitle>
<Database className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{totalRepos}</div>
<p className="text-xs text-muted-foreground">
{formatBytes(usedStorage)} / {formatBytes(totalStorage)} used
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Active Alerts</CardTitle>
<AlertTriangle className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{unacknowledgedAlerts}</div>
<p className="text-xs text-muted-foreground">
{unacknowledgedAlerts === 0 ? 'All clear' : 'Requires attention'}
</p>
</CardContent>
</Card>
{metrics && (
<>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">iSCSI Targets</CardTitle>
<HardDrive className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{metrics.scst.total_targets}</div>
<p className="text-xs text-muted-foreground">
{metrics.scst.active_sessions} active sessions
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Tasks</CardTitle>
<Activity className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{metrics.tasks.running_tasks}</div>
<p className="text-xs text-muted-foreground">
{metrics.tasks.pending_tasks} pending
</p>
</CardContent>
</Card>
</>
)}
</div>
{/* Quick Actions */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle>Quick Actions</CardTitle>
<CardDescription>Common operations</CardDescription>
</CardHeader>
<CardContent className="space-y-2">
<Link to="/storage" className="w-full">
<Button variant="outline" className="w-full justify-start">
<Database className="h-4 w-4 mr-2" />
Manage Storage
</Button>
</Link>
<Link to="/alerts" className="w-full">
<Button variant="outline" className="w-full justify-start">
<AlertTriangle className="h-4 w-4 mr-2" />
View Alerts
</Button>
</Link>
</CardContent>
</Card>
{/* Recent Alerts */}
{alerts && alerts.alerts.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Recent Alerts</CardTitle>
<CardDescription>Latest unacknowledged alerts</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-2">
{alerts.alerts.slice(0, 3).map((alert) => (
<div key={alert.id} className="text-sm">
<p className="font-medium">{alert.title}</p>
<p className="text-xs text-gray-500">{alert.message}</p>
</div>
))}
{alerts.alerts.length > 3 && (
<Link to="/alerts">
<Button variant="link" className="p-0 h-auto">
View all alerts
</Button>
</Link>
<div className="min-h-screen bg-background-dark text-white overflow-hidden">
{/* Header */}
<header className="flex-none px-6 py-5 border-b border-border-dark bg-background-dark/95 backdrop-blur z-10">
<div className="flex flex-wrap justify-between items-end gap-3 max-w-[1600px] mx-auto">
<div className="flex flex-col gap-1">
<h2 className="text-white text-3xl font-black tracking-tight">System Monitor</h2>
<p className="text-text-secondary text-sm">Real-time telemetry, storage health, and system event logs</p>
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2 px-3 py-2 bg-card-dark rounded-lg border border-border-dark">
<span className="relative flex h-2 w-2">
{isHealthy && (
<>
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500"></span>
</>
)}
{!isHealthy && (
<span className="relative inline-flex rounded-full h-2 w-2 bg-yellow-500"></span>
)}
</span>
<span className={`text-xs font-medium ${isHealthy ? 'text-emerald-400' : 'text-yellow-400'}`}>
{systemStatus}
</span>
</div>
<button className="flex items-center gap-2 h-10 px-4 bg-card-dark hover:bg-[#233648] border border-border-dark text-white text-sm font-bold rounded-lg transition-colors">
<RefreshCw className="h-4 w-4" />
<span>Refresh: {refreshInterval}s</span>
</button>
</div>
</div>
</header>
{/* Scrollable Content */}
<div className="flex-1 overflow-y-auto custom-scrollbar p-6">
<div className="flex flex-col gap-6 max-w-[1600px] mx-auto pb-10">
{/* Top Stats Row */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{/* CPU */}
<div className="flex flex-col gap-2 rounded-xl p-5 border border-border-dark bg-card-dark">
<div className="flex justify-between items-start">
<p className="text-text-secondary text-sm font-medium">CPU Load</p>
<Cpu className="text-text-secondary w-5 h-5" />
</div>
</CardContent>
</Card>
)}
<div className="flex items-end gap-3 mt-1">
<p className="text-white text-3xl font-bold">
{metrics?.system?.cpu_usage_percent?.toFixed(0) || 0}%
</p>
<span className="text-emerald-500 text-sm font-medium mb-1 flex items-center">
<TrendingDown className="w-4 h-4 mr-1" />
2%
</span>
</div>
<div className="h-1.5 w-full bg-[#233648] rounded-full mt-3 overflow-hidden">
<div
className="h-full bg-primary rounded-full transition-all"
style={{ width: `${metrics?.system?.cpu_usage_percent || 0}%` }}
></div>
</div>
</div>
{/* RAM */}
<div className="flex flex-col gap-2 rounded-xl p-5 border border-border-dark bg-card-dark">
<div className="flex justify-between items-start">
<p className="text-text-secondary text-sm font-medium">RAM Usage</p>
<MemoryStick className="text-text-secondary w-5 h-5" />
</div>
<div className="flex items-end gap-3 mt-1">
<p className="text-white text-3xl font-bold">
{formatBytes(metrics?.system?.memory_used_bytes || 0, 1)}
</p>
<span className="text-text-secondary text-xs mb-2">
/ {formatBytes(metrics?.system?.memory_total_bytes || 0, 1)}
</span>
</div>
<div className="h-1.5 w-full bg-[#233648] rounded-full mt-3 overflow-hidden">
<div
className="h-full bg-emerald-500 rounded-full transition-all"
style={{ width: `${metrics?.system?.memory_usage_percent || 0}%` }}
></div>
</div>
</div>
{/* Storage Health */}
<div className="flex flex-col gap-2 rounded-xl p-5 border border-border-dark bg-card-dark">
<div className="flex justify-between items-start">
<p className="text-text-secondary text-sm font-medium">Storage Status</p>
<CheckCircle2 className="text-emerald-500 w-5 h-5" />
</div>
<div className="flex items-end gap-3 mt-1">
<p className="text-white text-3xl font-bold">Online</p>
<span className="text-text-secondary text-sm font-medium mb-1">No Errors</span>
</div>
<div className="flex gap-1 mt-3">
<div className="h-1.5 flex-1 bg-emerald-500 rounded-l-full"></div>
<div className="h-1.5 flex-1 bg-emerald-500"></div>
<div className="h-1.5 flex-1 bg-emerald-500"></div>
<div className="h-1.5 flex-1 bg-emerald-500 rounded-r-full"></div>
</div>
</div>
{/* Uptime */}
<div className="flex flex-col gap-2 rounded-xl p-5 border border-border-dark bg-card-dark">
<div className="flex justify-between items-start">
<p className="text-text-secondary text-sm font-medium">System Uptime</p>
<Clock className="text-text-secondary w-5 h-5" />
</div>
<div className="mt-1">
<p className="text-white text-3xl font-bold">
{days}d {hours}h {minutes}m
</p>
</div>
<p className="text-text-secondary text-xs mt-3">Last reboot: Manual Patching</p>
</div>
</div>
{/* Middle Section: Charts & Storage */}
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
{/* Charts Column (2/3) */}
<div className="xl:col-span-2 flex flex-col gap-6">
{/* Network Chart */}
<div className="bg-card-dark border border-border-dark rounded-xl p-6 shadow-sm">
<div className="flex justify-between items-center mb-6">
<div>
<h3 className="text-white text-lg font-bold">Network Throughput</h3>
<p className="text-text-secondary text-sm">Inbound vs Outbound (eth0)</p>
</div>
<div className="text-right">
<p className="text-white text-2xl font-bold leading-tight">
{(currentThroughput.total / 1000).toFixed(1)} Gbps
</p>
<p className="text-emerald-500 text-sm">Peak: {(peakThroughput / 1000).toFixed(1)} Gbps</p>
</div>
</div>
<div className="h-[200px] w-full">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={networkDataPoints} margin={{ top: 5, right: 10, left: 0, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#324d67" opacity={0.3} />
<XAxis
dataKey="time"
stroke="#92adc9"
style={{ fontSize: '11px' }}
tick={{ fill: '#92adc9' }}
/>
<YAxis
stroke="#92adc9"
style={{ fontSize: '11px' }}
tick={{ fill: '#92adc9' }}
label={{ value: 'Mbps', angle: -90, position: 'insideLeft', fill: '#92adc9', style: { fontSize: '11px' } }}
/>
<Tooltip
contentStyle={{
backgroundColor: '#1a2632',
border: '1px solid #324d67',
borderRadius: '8px',
color: '#fff',
}}
labelStyle={{ color: '#92adc9', fontSize: '12px' }}
itemStyle={{ color: '#fff', fontSize: '12px' }}
formatter={(value: number) => [`${value} Mbps`, '']}
/>
<Legend
wrapperStyle={{ fontSize: '12px', color: '#92adc9' }}
iconType="line"
/>
<Line
type="monotone"
dataKey="inbound"
stroke="#137fec"
strokeWidth={2}
dot={false}
name="Inbound"
activeDot={{ r: 4 }}
/>
<Line
type="monotone"
dataKey="outbound"
stroke="#10b981"
strokeWidth={2}
dot={false}
name="Outbound"
activeDot={{ r: 4 }}
/>
</LineChart>
</ResponsiveContainer>
</div>
</div>
{/* Storage Chart */}
<div className="bg-card-dark border border-border-dark rounded-xl p-6 shadow-sm">
<div className="flex justify-between items-center mb-6">
<div>
<h3 className="text-white text-lg font-bold">Storage Capacity</h3>
<p className="text-text-secondary text-sm">Repository usage</p>
</div>
<div className="text-right">
<p className="text-white text-2xl font-bold leading-tight">
{storagePercent.toFixed(1)}%
</p>
<p className="text-text-secondary text-sm">Target: &lt;90%</p>
</div>
</div>
<div className="h-[150px] w-full relative">
<div className="w-full h-full bg-[#111a22] rounded flex items-center justify-center">
<div className="w-full px-4">
<div className="w-full bg-[#233648] h-2 rounded-full overflow-hidden">
<div
className="bg-primary h-full rounded-full transition-all"
style={{ width: `${storagePercent}%` }}
></div>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Storage Info Column (1/3) */}
<div className="flex flex-col gap-6">
<div className="bg-card-dark border border-border-dark rounded-xl p-6 h-full shadow-sm flex flex-col">
<div className="flex justify-between items-center mb-4">
<h3 className="text-white text-lg font-bold">Storage Overview</h3>
<span className="bg-[#233648] text-white text-xs px-2 py-1 rounded border border-border-dark">
{repositories?.length || 0} Repos
</span>
</div>
<div className="mt-4 pt-4 border-t border-border-dark">
<div className="flex justify-between text-sm text-text-secondary">
<span>Total Capacity</span>
<span className="text-white font-bold">{formatBytes(totalStorage)}</span>
</div>
<div className="w-full bg-[#233648] h-2 rounded-full mt-2 overflow-hidden">
<div
className="bg-primary h-full transition-all"
style={{ width: `${storagePercent}%` }}
></div>
</div>
<div className="flex justify-between text-xs text-text-secondary mt-1">
<span>Used: {formatBytes(usedStorage)}</span>
<span>Free: {formatBytes(totalStorage - usedStorage)}</span>
</div>
</div>
</div>
</div>
</div>
{/* Bottom Section: Tabs & Logs */}
<div className="bg-card-dark border border-border-dark rounded-xl shadow-sm overflow-hidden flex flex-col h-[400px]">
{/* Tabs Header */}
<div className="flex border-b border-border-dark bg-[#161f29]">
<button
onClick={() => setActiveTab('jobs')}
className={`px-6 py-4 text-sm font-bold transition-colors ${
activeTab === 'jobs'
? 'text-primary border-b-2 border-primary bg-card-dark'
: 'text-text-secondary hover:text-white'
}`}
>
Active Jobs{' '}
{activeJobs.length > 0 && (
<span className="ml-2 bg-primary/20 text-primary px-1.5 py-0.5 rounded text-xs">
{activeJobs.length}
</span>
)}
</button>
<button
onClick={() => setActiveTab('logs')}
className={`px-6 py-4 text-sm font-medium transition-colors ${
activeTab === 'logs'
? 'text-primary border-b-2 border-primary bg-card-dark'
: 'text-text-secondary hover:text-white'
}`}
>
System Logs
</button>
<button
onClick={() => setActiveTab('alerts')}
className={`px-6 py-4 text-sm font-medium transition-colors ${
activeTab === 'alerts'
? 'text-primary border-b-2 border-primary bg-card-dark'
: 'text-text-secondary hover:text-white'
}`}
>
Alerts History
</button>
<div className="flex-1 flex justify-end items-center px-4">
<div className="relative">
<Activity className="absolute left-2 top-1.5 text-text-secondary w-4 h-4" />
<input
className="bg-[#111a22] border border-border-dark rounded-md py-1 pl-8 pr-3 text-sm text-white focus:outline-none focus:border-primary w-48 transition-all"
placeholder="Search logs..."
type="text"
/>
</div>
</div>
</div>
{/* Tab Content */}
<div className="flex-1 overflow-hidden flex flex-col">
{activeTab === 'jobs' && (
<div className="p-0">
<table className="w-full text-left border-collapse">
<thead className="bg-[#1a2632] text-xs uppercase text-text-secondary font-medium sticky top-0 z-10">
<tr>
<th className="px-6 py-3 border-b border-border-dark">Job Name</th>
<th className="px-6 py-3 border-b border-border-dark">Type</th>
<th className="px-6 py-3 border-b border-border-dark w-1/3">Progress</th>
<th className="px-6 py-3 border-b border-border-dark">Speed</th>
<th className="px-6 py-3 border-b border-border-dark">Status</th>
</tr>
</thead>
<tbody className="text-sm divide-y divide-border-dark">
{activeJobs.map((job) => (
<tr key={job.id} className="group hover:bg-[#233648] transition-colors">
<td className="px-6 py-4 font-medium text-white">{job.name}</td>
<td className="px-6 py-4 text-text-secondary">{job.type}</td>
<td className="px-6 py-4">
<div className="flex items-center gap-3">
<div className="w-full bg-[#111a22] rounded-full h-2 overflow-hidden">
<div
className="bg-primary h-full rounded-full relative overflow-hidden"
style={{ width: `${job.progress}%` }}
>
<div className="absolute inset-0 bg-white/20 animate-pulse"></div>
</div>
</div>
<span className="text-xs font-mono text-white">{job.progress}%</span>
</div>
{job.eta && (
<p className="text-[10px] text-text-secondary mt-1">ETA: {job.eta}</p>
)}
</td>
<td className="px-6 py-4 text-text-secondary font-mono">{job.speed}</td>
<td className="px-6 py-4">
<span className="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-primary/20 text-primary">
Running
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{activeTab === 'logs' && (
<>
<div className="px-6 py-2 bg-[#161f29] border-y border-border-dark flex items-center justify-between">
<h4 className="text-xs uppercase text-text-secondary font-bold tracking-wider">
Recent System Events
</h4>
<button className="text-xs text-primary hover:text-white transition-colors">
View All Logs
</button>
</div>
<div className="flex-1 overflow-y-auto custom-scrollbar bg-[#111a22]">
<table className="w-full text-left border-collapse">
<tbody className="text-sm font-mono divide-y divide-border-dark/50">
{systemLogs.map((log, idx) => (
<tr key={idx} className="group hover:bg-[#233648] transition-colors">
<td className="px-6 py-2 text-text-secondary w-32 whitespace-nowrap">
{log.time}
</td>
<td className="px-6 py-2 w-24">
<span
className={
log.level === 'INFO'
? 'text-emerald-500'
: log.level === 'WARN'
? 'text-yellow-500'
: 'text-red-500'
}
>
{log.level}
</span>
</td>
<td className="px-6 py-2 w-32 text-white">{log.source}</td>
<td className="px-6 py-2 text-text-secondary truncate max-w-lg">
{log.message}
</td>
</tr>
))}
</tbody>
</table>
</div>
</>
)}
{activeTab === 'alerts' && (
<div className="flex-1 overflow-y-auto custom-scrollbar bg-[#111a22] p-6">
{alerts?.alerts && alerts.alerts.length > 0 ? (
<div className="space-y-3">
{alerts.alerts.map((alert) => (
<div
key={alert.id}
className="bg-[#1a2632] border border-border-dark rounded-lg p-4 hover:bg-[#233648] transition-colors"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<AlertTriangle
className={`w-4 h-4 ${
alert.severity === 'critical'
? 'text-red-500'
: alert.severity === 'warning'
? 'text-yellow-500'
: 'text-blue-500'
}`}
/>
<h4 className="text-white font-medium">{alert.title}</h4>
<span
className={`px-2 py-0.5 rounded text-xs font-medium ${
alert.severity === 'critical'
? 'bg-red-500/20 text-red-400'
: alert.severity === 'warning'
? 'bg-yellow-500/20 text-yellow-400'
: 'bg-blue-500/20 text-blue-400'
}`}
>
{alert.severity}
</span>
</div>
<p className="text-text-secondary text-sm">{alert.message}</p>
<p className="text-text-secondary text-xs mt-2 font-mono">
{new Date(alert.created_at).toLocaleString()}
</p>
</div>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-12">
<CheckCircle2 className="w-12 h-12 text-emerald-500 mx-auto mb-4" />
<p className="text-text-secondary">No alerts</p>
</div>
)}
</div>
)}
</div>
</div>
</div>
</div>
</div>
)
}

View File

@@ -25,17 +25,17 @@ export default function ISCSITargetDetail() {
})
if (isLoading) {
return <div className="text-sm text-gray-500">Loading target details...</div>
return <div className="text-sm text-text-secondary min-h-screen bg-background-dark p-6">Loading target details...</div>
}
if (!data) {
return <div className="text-sm text-red-500">Target not found</div>
return <div className="text-sm text-red-400 min-h-screen bg-background-dark p-6">Target not found</div>
}
const { target, luns } = data
return (
<div className="space-y-6">
<div className="space-y-6 min-h-screen bg-background-dark p-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
@@ -44,9 +44,9 @@ export default function ISCSITargetDetail() {
Back
</Button>
<div>
<h1 className="text-3xl font-bold text-gray-900 font-mono text-lg">{target.iqn}</h1>
<h1 className="text-3xl font-bold text-white font-mono text-lg">{target.iqn}</h1>
{target.alias && (
<p className="mt-1 text-sm text-gray-600">{target.alias}</p>
<p className="mt-1 text-sm text-text-secondary">{target.alias}</p>
)}
</div>
</div>
@@ -70,14 +70,14 @@ export default function ISCSITargetDetail() {
<CardContent>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-gray-500">Status:</span>
<span className={target.is_active ? 'text-green-600' : 'text-gray-600'}>
<span className="text-text-secondary">Status:</span>
<span className={target.is_active ? 'text-green-400' : 'text-text-secondary'}>
{target.is_active ? 'Active' : 'Inactive'}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">IQN:</span>
<span className="font-mono text-xs">{target.iqn}</span>
<span className="text-text-secondary">IQN:</span>
<span className="font-mono text-xs text-white">{target.iqn}</span>
</div>
</div>
</CardContent>
@@ -90,12 +90,12 @@ export default function ISCSITargetDetail() {
<CardContent>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-gray-500">Total LUNs:</span>
<span className="font-medium">{luns.length}</span>
<span className="text-text-secondary">Total LUNs:</span>
<span className="font-medium text-white">{luns.length}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">Active:</span>
<span className="font-medium">
<span className="text-text-secondary">Active:</span>
<span className="font-medium text-white">
{luns.filter((l) => l.is_active).length}
</span>
</div>
@@ -150,38 +150,38 @@ export default function ISCSITargetDetail() {
{luns.length > 0 ? (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<thead className="bg-[#1a2632]">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
<th className="px-6 py-3 text-left text-xs font-medium text-text-secondary uppercase">
LUN #
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
<th className="px-6 py-3 text-left text-xs font-medium text-text-secondary uppercase">
Handler
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
<th className="px-6 py-3 text-left text-xs font-medium text-text-secondary uppercase">
Device Path
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
<th className="px-6 py-3 text-left text-xs font-medium text-text-secondary uppercase">
Type
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
<th className="px-6 py-3 text-left text-xs font-medium text-text-secondary uppercase">
Status
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
<tbody className="bg-card-dark divide-y divide-border-dark">
{luns.map((lun) => (
<tr key={lun.id}>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<tr key={lun.id} className="hover:bg-[#233648]">
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-white">
{lun.lun_number}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">
{lun.handler}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-mono text-xs">
<td className="px-6 py-4 whitespace-nowrap text-sm font-mono text-xs text-white">
{lun.device_path}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">
{lun.device_type}
</td>
<td className="px-6 py-4 whitespace-nowrap">
@@ -203,7 +203,7 @@ export default function ISCSITargetDetail() {
) : (
<div className="text-center py-8">
<HardDrive className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<p className="text-sm text-gray-500 mb-4">No LUNs configured</p>
<p className="text-sm text-text-secondary mb-4">No LUNs configured</p>
<Button variant="outline" onClick={() => setShowAddLUN(true)}>
<Plus className="h-4 w-4 mr-2" />
Add First LUN
@@ -414,7 +414,7 @@ function AddInitiatorForm({ targetId, onClose, onSuccess }: AddInitiatorFormProp
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 font-mono text-sm"
required
/>
<p className="mt-1 text-xs text-gray-500">
<p className="mt-1 text-xs text-text-secondary">
Format: iqn.YYYY-MM.reverse.domain:identifier
</p>
</div>

View File

@@ -24,11 +24,11 @@ export default function ISCSITargets() {
})
return (
<div className="space-y-6">
<div className="space-y-6 min-h-screen bg-background-dark p-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">iSCSI Targets</h1>
<p className="mt-2 text-sm text-gray-600">
<h1 className="text-3xl font-bold text-white">iSCSI Targets</h1>
<p className="mt-2 text-sm text-text-secondary">
Manage SCST iSCSI targets, LUNs, and initiators
</p>
</div>
@@ -61,7 +61,7 @@ export default function ISCSITargets() {
{/* Targets List */}
{isLoading ? (
<p className="text-sm text-gray-500">Loading targets...</p>
<p className="text-sm text-text-secondary">Loading targets...</p>
) : targets && targets.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{targets.map((target) => (
@@ -72,8 +72,8 @@ export default function ISCSITargets() {
<Card>
<CardContent className="p-12 text-center">
<Server className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No iSCSI Targets</h3>
<p className="text-sm text-gray-500 mb-4">
<h3 className="text-lg font-medium text-white mb-2">No iSCSI Targets</h3>
<p className="text-sm text-text-secondary mb-4">
Create your first iSCSI target to start exporting storage
</p>
<Button onClick={() => setShowCreateForm(true)}>
@@ -111,14 +111,14 @@ function TargetCard({ target }: TargetCardProps) {
<CardContent>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-500">Status:</span>
<span className={target.is_active ? 'text-green-600' : 'text-gray-600'}>
<span className="text-text-secondary">Status:</span>
<span className={target.is_active ? 'text-green-400' : 'text-text-secondary'}>
{target.is_active ? 'Active' : 'Inactive'}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">Created:</span>
<span className="text-gray-700">
<span className="text-text-secondary">Created:</span>
<span className="text-white">
{new Date(target.created_at).toLocaleDateString()}
</span>
</div>
@@ -183,7 +183,7 @@ function CreateTargetForm({ onClose, onSuccess }: CreateTargetFormProps) {
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
required
/>
<p className="mt-1 text-xs text-gray-500">
<p className="mt-1 text-xs text-text-secondary">
Format: iqn.YYYY-MM.reverse.domain:identifier
</p>
</div>

View File

@@ -29,20 +29,20 @@ export default function LoginPage() {
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full space-y-8 p-8 bg-white rounded-lg shadow-md">
<div className="min-h-screen flex items-center justify-center bg-background-dark">
<div className="max-w-md w-full space-y-8 p-8 bg-card-dark border border-border-dark rounded-lg shadow-md">
<div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
<h2 className="mt-6 text-center text-3xl font-extrabold text-white">
AtlasOS - Calypso
</h2>
<p className="mt-2 text-center text-sm text-gray-600">
<p className="mt-2 text-center text-sm text-text-secondary">
Sign in to your account
</p>
</div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
{error && (
<div className="rounded-md bg-red-50 p-4">
<p className="text-sm text-red-800">{error}</p>
<div className="rounded-md bg-red-500/10 border border-red-500/30 p-4">
<p className="text-sm text-red-400 font-medium">{error}</p>
</div>
)}
<div className="rounded-md shadow-sm -space-y-px">
@@ -55,7 +55,7 @@ export default function LoginPage() {
name="username"
type="text"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-border-dark bg-[#111a22] placeholder-text-secondary text-white rounded-t-md focus:outline-none focus:ring-primary focus:border-primary focus:z-10 sm:text-sm"
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
@@ -70,7 +70,7 @@ export default function LoginPage() {
name="password"
type="password"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-border-dark bg-[#111a22] placeholder-text-secondary text-white rounded-b-md focus:outline-none focus:ring-primary focus:border-primary focus:z-10 sm:text-sm"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
@@ -82,9 +82,22 @@ export default function LoginPage() {
<button
type="submit"
disabled={loginMutation.isPending}
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
className="relative group relative w-full flex justify-center py-2.5 px-4 border border-primary/30 bg-card-dark text-white text-sm font-bold rounded-lg hover:bg-[#233648] transition-all overflow-hidden electric-glow electric-glow-border disabled:opacity-50 disabled:cursor-not-allowed"
>
{loginMutation.isPending ? 'Signing in...' : 'Sign in'}
{/* Electric glow gradient overlay */}
<span className="absolute inset-0 bg-gradient-to-r from-primary/0 via-primary/15 to-primary/0 opacity-60"></span>
{/* Shimmer effect */}
<span className="absolute inset-0 bg-gradient-to-r from-transparent via-white/5 to-transparent animate-[shimmer_3s_infinite]"></span>
<span className="relative z-10 flex items-center gap-2">
{loginMutation.isPending ? (
<>
<span className="material-symbols-outlined text-[18px] animate-spin">refresh</span>
Signing in...
</>
) : (
'Sign in'
)}
</span>
</button>
</div>
</form>

File diff suppressed because it is too large Load Diff

View File

@@ -22,11 +22,11 @@ export default function TapeLibraries() {
})
return (
<div className="space-y-6">
<div className="space-y-6 min-h-screen bg-background-dark p-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">Tape Libraries</h1>
<p className="mt-2 text-sm text-gray-600">
<h1 className="text-3xl font-bold text-white">Tape Libraries</h1>
<p className="mt-2 text-sm text-text-secondary">
Manage physical and virtual tape libraries
</p>
</div>
@@ -49,14 +49,14 @@ export default function TapeLibraries() {
</div>
{/* Tabs */}
<div className="border-b border-gray-200">
<div className="border-b border-border-dark">
<nav className="-mb-px flex space-x-8">
<button
onClick={() => setActiveTab('vtl')}
className={`py-4 px-1 border-b-2 font-medium text-sm ${
activeTab === 'vtl'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
? 'border-primary text-primary'
: 'border-transparent text-text-secondary hover:text-white hover:border-border-dark'
}`}
>
Virtual Tape Libraries ({vtlLibraries?.length || 0})
@@ -65,8 +65,8 @@ export default function TapeLibraries() {
onClick={() => setActiveTab('physical')}
className={`py-4 px-1 border-b-2 font-medium text-sm ${
activeTab === 'physical'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
? 'border-primary text-primary'
: 'border-transparent text-text-secondary hover:text-white hover:border-border-dark'
}`}
>
Physical Libraries ({physicalLibraries?.length || 0})
@@ -78,7 +78,7 @@ export default function TapeLibraries() {
{activeTab === 'vtl' && (
<div>
{loadingVTL ? (
<p className="text-sm text-gray-500">Loading VTL libraries...</p>
<p className="text-sm text-text-secondary">Loading VTL libraries...</p>
) : vtlLibraries && vtlLibraries.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{vtlLibraries.map((library: VirtualTapeLibrary) => (
@@ -89,8 +89,8 @@ export default function TapeLibraries() {
<Card>
<CardContent className="p-12 text-center">
<HardDrive className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No Virtual Tape Libraries</h3>
<p className="text-sm text-gray-500 mb-4">
<h3 className="text-lg font-medium text-white mb-2">No Virtual Tape Libraries</h3>
<p className="text-sm text-text-secondary mb-4">
Create your first virtual tape library to get started
</p>
<Link to="/tape/vtl/create">
@@ -108,7 +108,7 @@ export default function TapeLibraries() {
{activeTab === 'physical' && (
<div>
{loadingPhysical ? (
<p className="text-sm text-gray-500">Loading physical libraries...</p>
<p className="text-sm text-text-secondary">Loading physical libraries...</p>
) : physicalLibraries && physicalLibraries.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{physicalLibraries.map((library: PhysicalTapeLibrary) => (
@@ -119,8 +119,8 @@ export default function TapeLibraries() {
<Card>
<CardContent className="p-12 text-center">
<HardDrive className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No Physical Tape Libraries</h3>
<p className="text-sm text-gray-500 mb-4">
<h3 className="text-lg font-medium text-white mb-2">No Physical Tape Libraries</h3>
<p className="text-sm text-text-secondary mb-4">
Discover physical tape libraries connected to the system
</p>
<Button variant="outline">
@@ -168,17 +168,17 @@ function LibraryCard({ library, type }: LibraryCardProps) {
<CardContent>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-500">Slots:</span>
<span className="font-medium">{library.slot_count}</span>
<span className="text-text-secondary">Slots:</span>
<span className="font-medium text-white">{library.slot_count}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">Drives:</span>
<span className="font-medium">{library.drive_count}</span>
<span className="text-text-secondary">Drives:</span>
<span className="font-medium text-white">{library.drive_count}</span>
</div>
{isPhysical && 'vendor' in library && library.vendor && (
<div className="flex justify-between">
<span className="text-gray-500">Vendor:</span>
<span className="font-medium">{library.vendor} {library.model}</span>
<span className="text-text-secondary">Vendor:</span>
<span className="font-medium text-white">{library.vendor} {library.model}</span>
</div>
)}
</div>

View File

@@ -28,17 +28,17 @@ export default function VTLDetail() {
})
if (isLoading) {
return <div className="text-sm text-gray-500">Loading library details...</div>
return <div className="text-sm text-text-secondary min-h-screen bg-background-dark p-6">Loading library details...</div>
}
if (!data) {
return <div className="text-sm text-red-500">Library not found</div>
return <div className="text-sm text-red-400 min-h-screen bg-background-dark p-6">Library not found</div>
}
const { library, drives, tapes } = data
return (
<div className="space-y-6">
<div className="space-y-6 min-h-screen bg-background-dark p-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
@@ -47,8 +47,8 @@ export default function VTLDetail() {
Back
</Button>
<div>
<h1 className="text-3xl font-bold text-gray-900">{library.name}</h1>
<p className="mt-1 text-sm text-gray-600">
<h1 className="text-3xl font-bold text-white">{library.name}</h1>
<p className="mt-1 text-sm text-text-secondary">
Virtual Tape Library {library.slot_count} slots {library.drive_count} drives
</p>
</div>
@@ -75,18 +75,18 @@ export default function VTLDetail() {
<CardContent>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-gray-500">Status:</span>
<span className={library.is_active ? 'text-green-600' : 'text-gray-600'}>
<span className="text-text-secondary">Status:</span>
<span className={library.is_active ? 'text-green-400' : 'text-text-secondary'}>
{library.is_active ? 'Active' : 'Inactive'}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">mhVTL ID:</span>
<span className="font-medium">{library.mhvtl_library_id}</span>
<span className="text-text-secondary">mhVTL ID:</span>
<span className="font-medium text-white">{library.mhvtl_library_id}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">Storage Path:</span>
<span className="font-mono text-xs">{library.storage_path}</span>
<span className="text-text-secondary">Storage Path:</span>
<span className="font-mono text-xs text-white">{library.storage_path}</span>
</div>
</div>
</CardContent>
@@ -99,16 +99,16 @@ export default function VTLDetail() {
<CardContent>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-gray-500">Total Slots:</span>
<span className="font-medium">{library.slot_count}</span>
<span className="text-text-secondary">Total Slots:</span>
<span className="font-medium text-white">{library.slot_count}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">Used Slots:</span>
<span className="font-medium">{tapes.length}</span>
<span className="text-text-secondary">Used Slots:</span>
<span className="font-medium text-white">{tapes.length}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">Free Slots:</span>
<span className="font-medium">{library.slot_count - tapes.length}</span>
<span className="text-text-secondary">Free Slots:</span>
<span className="font-medium text-white">{library.slot_count - tapes.length}</span>
</div>
</div>
</CardContent>
@@ -121,18 +121,18 @@ export default function VTLDetail() {
<CardContent>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-gray-500">Total Drives:</span>
<span className="font-medium">{library.drive_count}</span>
<span className="text-text-secondary">Total Drives:</span>
<span className="font-medium text-white">{library.drive_count}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">Idle:</span>
<span className="font-medium">
<span className="text-text-secondary">Idle:</span>
<span className="font-medium text-white">
{drives.filter((d) => d.status === 'idle').length}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">Ready:</span>
<span className="font-medium">
<span className="text-text-secondary">Ready:</span>
<span className="font-medium text-white">
{drives.filter((d) => d.status === 'ready').length}
</span>
</div>
@@ -169,7 +169,7 @@ export default function VTLDetail() {
))}
</div>
) : (
<p className="text-sm text-gray-500">No drives configured</p>
<p className="text-sm text-text-secondary">No drives configured</p>
)}
</CardContent>
</Card>
@@ -194,35 +194,35 @@ export default function VTLDetail() {
{tapes.length > 0 ? (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<thead className="bg-[#1a2632]">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
<th className="px-6 py-3 text-left text-xs font-medium text-text-secondary uppercase">
Barcode
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
<th className="px-6 py-3 text-left text-xs font-medium text-text-secondary uppercase">
Slot
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
<th className="px-6 py-3 text-left text-xs font-medium text-text-secondary uppercase">
Size
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
<th className="px-6 py-3 text-left text-xs font-medium text-text-secondary uppercase">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
<th className="px-6 py-3 text-left text-xs font-medium text-text-secondary uppercase">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
<tbody className="bg-card-dark divide-y divide-border-dark">
{tapes.map((tape) => (
<tr key={tape.id}>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<tr key={tape.id} className="hover:bg-[#233648]">
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-white">
{tape.barcode}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">
{tape.slot_number}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">
{formatBytes(tape.size_bytes)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
@@ -259,7 +259,7 @@ export default function VTLDetail() {
</table>
</div>
) : (
<p className="text-sm text-gray-500">No tapes created yet</p>
<p className="text-sm text-text-secondary">No tapes created yet</p>
)}
</CardContent>
</Card>
@@ -299,12 +299,12 @@ function DriveCard({ drive, tapes, isSelected, onSelect }: DriveCardProps) {
</span>
</div>
{currentTape ? (
<div className="text-sm text-gray-600">
<p>Tape: {currentTape.barcode}</p>
<p className="text-xs text-gray-500">{formatBytes(currentTape.size_bytes)}</p>
<div className="text-sm text-text-secondary">
<p className="text-white">Tape: {currentTape.barcode}</p>
<p className="text-xs text-text-secondary">{formatBytes(currentTape.size_bytes)}</p>
</div>
) : (
<p className="text-sm text-gray-400">No tape loaded</p>
<p className="text-sm text-text-secondary">No tape loaded</p>
)}
{isSelected && (
<div className="mt-2 pt-2 border-t">

View File

@@ -14,7 +14,7 @@ export default {
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
DEFAULT: "#137fec",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
@@ -41,11 +41,20 @@ export default {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
// Dark theme colors from example
"background-dark": "#111a22",
"card-dark": "#1a2632",
"border-dark": "#324d67",
"text-secondary": "#92adc9",
},
fontFamily: {
display: ["Manrope", "sans-serif"],
mono: ["JetBrains Mono", "monospace"],
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
lg: "0.5rem",
xl: "0.75rem",
DEFAULT: "0.25rem",
},
},
},