519 lines
26 KiB
TypeScript
519 lines
26 KiB
TypeScript
import React, { useState } from 'react';
|
|
import {
|
|
HardDrive,
|
|
Cpu,
|
|
Layers,
|
|
FolderSync,
|
|
ShieldCheck,
|
|
Activity,
|
|
ArrowUpRight,
|
|
TrendingUp,
|
|
Zap,
|
|
} from 'lucide-react';
|
|
import { AppIcon } from '../components/AppIcon.js';
|
|
import type { SystemStatus, ZfsPool, InstalledApp } from '../types/index.js';
|
|
|
|
interface OverviewViewProps {
|
|
status: SystemStatus | null;
|
|
pools: ZfsPool[];
|
|
apps: InstalledApp[];
|
|
onNavigate: (tab: string) => void;
|
|
}
|
|
|
|
export const OverviewView: React.FC<OverviewViewProps> = ({
|
|
status,
|
|
pools,
|
|
apps,
|
|
onNavigate,
|
|
}) => {
|
|
const primaryPool = pools[0];
|
|
const [telemetryRange, setTelemetryRange] = useState<'15m' | '1h' | '6h' | '24h'>('1h');
|
|
|
|
// Sparkline data based on selected time range
|
|
const throughputData = {
|
|
'15m': [34, 45, 62, 50, 78, 110, 95, 82, 60, 88, 115, 142, 90, 75, 105],
|
|
'1h': [42, 65, 30, 85, 95, 120, 80, 45, 60, 90, 110, 75, 40, 95, 130],
|
|
'6h': [20, 35, 45, 80, 110, 145, 130, 95, 70, 85, 60, 50, 65, 90, 85],
|
|
'24h': [15, 25, 40, 65, 90, 120, 140, 110, 85, 70, 60, 55, 70, 80, 95],
|
|
}[telemetryRange];
|
|
|
|
const arcRatioData = {
|
|
'15m': [98.2, 98.4, 98.5, 98.3, 98.6, 98.8, 98.7, 98.5, 98.9, 98.6, 98.7, 98.6, 98.8, 98.7, 98.6],
|
|
'1h': [97.8, 98.1, 98.4, 98.2, 98.6, 98.8, 98.5, 98.7, 98.4, 98.9, 98.6, 98.7, 98.5, 98.8, 98.6],
|
|
'6h': [97.2, 97.6, 98.0, 98.3, 98.5, 98.7, 98.4, 98.6, 98.8, 98.5, 98.7, 98.6, 98.4, 98.6, 98.6],
|
|
'24h': [96.8, 97.2, 97.9, 98.1, 98.4, 98.6, 98.7, 98.5, 98.6, 98.7, 98.5, 98.6, 98.7, 98.8, 98.6],
|
|
}[telemetryRange];
|
|
|
|
const cpuData = {
|
|
'15m': [8, 12, 14, 22, 38, 18, 11, 9, 15, 18, 28, 15, 12, 14, 11],
|
|
'1h': [10, 14, 18, 25, 42, 19, 12, 10, 16, 20, 32, 16, 14, 15, 12],
|
|
'6h': [8, 10, 15, 30, 48, 25, 14, 12, 18, 22, 35, 18, 15, 16, 13],
|
|
'24h': [6, 8, 12, 28, 45, 30, 15, 11, 16, 24, 38, 20, 16, 18, 12],
|
|
}[telemetryRange];
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Top Banner / Hero Header with Portara Styling */}
|
|
<div className="relative overflow-hidden rounded-2xl bg-gradient-to-r from-slate-900 via-slate-900/90 to-cyan-950/40 border border-slate-800 p-6 shadow-xl">
|
|
<div className="absolute right-0 top-0 bottom-0 w-96 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-cyan-500/10 via-transparent to-transparent pointer-events-none" />
|
|
|
|
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-6 relative z-10">
|
|
<div className="flex items-start gap-4">
|
|
<div className="h-12 w-12 rounded-xl bg-slate-950 border border-cyan-500/30 p-2 flex items-center justify-center shadow-lg shadow-cyan-500/10 shrink-0">
|
|
<img src="/logo.svg" alt="NaxOS" className="h-full w-full object-contain filter drop-shadow-[0_2px_6px_rgba(12,179,229,0.4)]" />
|
|
</div>
|
|
<div>
|
|
<div className="flex items-center gap-2.5">
|
|
<h2 className="text-xl font-bold text-white tracking-tight">Appliance Overview</h2>
|
|
<span className="px-2 py-0.5 rounded-full text-[10px] font-semibold bg-cyan-500/15 text-cyan-300 border border-cyan-500/30 font-mono">
|
|
NixOS 26.05
|
|
</span>
|
|
<span className="px-2 py-0.5 rounded-full text-[10px] font-semibold bg-blue-500/10 text-blue-300 border border-blue-500/20 font-mono">
|
|
OpenZFS 2.2
|
|
</span>
|
|
</div>
|
|
<p className="text-xs text-slate-400 mt-1">
|
|
Active Node: <strong className="text-slate-200">{status?.hostname || 'naxos-storage'}</strong> · Generation <strong className="text-slate-200">{status?.nixosGeneration || 42}</strong> · GitOps Engine Synchronized
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3">
|
|
<button
|
|
onClick={() => onNavigate('storage')}
|
|
className="flex items-center gap-1.5 px-3.5 py-2 rounded-xl bg-slate-800/80 hover:bg-slate-700 text-xs font-semibold text-slate-200 transition border border-slate-700 shadow-sm"
|
|
>
|
|
<HardDrive className="h-3.5 w-3.5 text-cyan-400" />
|
|
<span>Manage Storage</span>
|
|
<ArrowUpRight className="h-3.5 w-3.5 text-slate-400" />
|
|
</button>
|
|
<button
|
|
onClick={() => onNavigate('apps')}
|
|
className="flex items-center gap-1.5 px-3.5 py-2 rounded-xl bg-cyan-600 hover:bg-cyan-500 text-xs font-semibold text-white transition shadow-sm shadow-cyan-600/30"
|
|
>
|
|
<Layers className="h-3.5 w-3.5" />
|
|
<span>App Store</span>
|
|
<ArrowUpRight className="h-3.5 w-3.5" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* KPI Cards Grid */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
{/* Storage Pool Card */}
|
|
<div className="p-5 rounded-2xl bg-slate-900/70 border border-slate-800/80 flex flex-col justify-between hover:border-slate-700 transition">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-xs text-slate-400 font-medium">Pool: {primaryPool?.name || 'tank'}</span>
|
|
<span className="px-2 py-0.5 rounded-full text-[10px] font-semibold bg-cyan-500/10 text-cyan-400 border border-cyan-500/20">
|
|
{primaryPool?.health || 'ONLINE'}
|
|
</span>
|
|
</div>
|
|
<div className="mt-4">
|
|
<div className="text-2xl font-bold text-white tracking-tight">
|
|
{primaryPool?.allocated || '8.2T'} <span className="text-xs text-slate-400 font-normal">/ {primaryPool?.size || '14.5T'}</span>
|
|
</div>
|
|
{/* Progress Bar */}
|
|
<div className="w-full bg-slate-800 h-2 rounded-full mt-2 overflow-hidden">
|
|
<div
|
|
className="bg-gradient-to-r from-cyan-500 to-blue-500 h-full rounded-full transition-all duration-500"
|
|
style={{ width: `${primaryPool?.capacityPercent || 56}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="mt-3 flex items-center justify-between text-[11px] text-slate-400">
|
|
<span>Free: {primaryPool?.free || '6.3T'}</span>
|
|
<span>Frag: {primaryPool?.fragmentation || '14%'}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* ZFS ARC Hit Ratio */}
|
|
<div className="p-5 rounded-2xl bg-slate-900/70 border border-slate-800/80 flex flex-col justify-between hover:border-slate-700 transition">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-xs text-slate-400 font-medium">ZFS ARC Cache</span>
|
|
<ShieldCheck className="h-4 w-4 text-cyan-400" />
|
|
</div>
|
|
<div className="mt-4">
|
|
<div className="text-2xl font-bold text-white tracking-tight flex items-baseline gap-2">
|
|
<span>{status?.arcHitRatioPercent || 98.6}%</span>
|
|
<span className="text-xs text-cyan-400 font-normal">Efficiency</span>
|
|
</div>
|
|
<p className="text-xs text-slate-400 mt-1">
|
|
ARC Size: <strong className="text-slate-200">4.0 GB</strong> (Target: 4.0 GB)
|
|
</p>
|
|
</div>
|
|
<div className="mt-3 text-[11px] text-cyan-400 flex items-center gap-1">
|
|
<TrendingUp className="h-3 w-3" />
|
|
<span>Optimal cache hit performance</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Workloads Card */}
|
|
<div className="p-5 rounded-2xl bg-slate-900/70 border border-slate-800/80 flex flex-col justify-between hover:border-slate-700 transition">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-xs text-slate-400 font-medium">Running Workloads</span>
|
|
<Layers className="h-4 w-4 text-sky-400" />
|
|
</div>
|
|
<div className="mt-4">
|
|
<div className="text-2xl font-bold text-white tracking-tight">
|
|
{apps.length || 2} <span className="text-xs text-slate-400 font-normal">Active Services</span>
|
|
</div>
|
|
<div className="flex items-center gap-1.5 mt-2.5">
|
|
<div className="p-1 rounded-lg bg-slate-950 border border-slate-800" title="Immich Photo Hub">
|
|
<AppIcon name="immich" className="h-5 w-5" />
|
|
</div>
|
|
<div className="p-1 rounded-lg bg-slate-950 border border-slate-800" title="Jellyfin Media Server">
|
|
<AppIcon name="jellyfin" className="h-5 w-5" />
|
|
</div>
|
|
<div className="p-1 rounded-lg bg-slate-950 border border-slate-800" title="Telemetry Collector">
|
|
<AppIcon name="grafana" className="h-5 w-5" />
|
|
</div>
|
|
<div className="p-1 rounded-lg bg-slate-950 border border-slate-800" title="Docker Engine">
|
|
<AppIcon name="docker" className="h-5 w-5" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="mt-3 text-[11px] text-slate-400">
|
|
Runtime Engine: <span className="text-slate-200">Docker + Systemd</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Network Shares */}
|
|
<div className="p-5 rounded-2xl bg-slate-900/70 border border-slate-800/80 flex flex-col justify-between hover:border-slate-700 transition">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-xs text-slate-400 font-medium">Active File Shares</span>
|
|
<FolderSync className="h-4 w-4 text-blue-400" />
|
|
</div>
|
|
<div className="mt-4">
|
|
<div className="text-2xl font-bold text-white tracking-tight">
|
|
{status?.activeSharesCount || 3}
|
|
</div>
|
|
<p className="text-xs text-slate-400 mt-1">
|
|
SMB (Time Machine) + NFS v4
|
|
</p>
|
|
</div>
|
|
<div className="mt-3 text-[11px] text-slate-400">
|
|
Bonjour / mDNS discovery active
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* SEAMLESS LIVE TELEMETRY & SYSTEM PERFORMANCE */}
|
|
<div className="p-6 rounded-2xl bg-slate-900/70 border border-slate-800 space-y-5">
|
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 border-b border-slate-800/80 pb-4">
|
|
<div className="flex items-center gap-2.5">
|
|
<div className="p-2 rounded-xl bg-cyan-500/10 border border-cyan-500/20 text-cyan-400">
|
|
<Activity className="h-4 w-4" />
|
|
</div>
|
|
<div>
|
|
<div className="flex items-center gap-2">
|
|
<h3 className="text-sm font-bold text-white">Live System & Storage Telemetry</h3>
|
|
<span className="flex h-2 w-2 relative">
|
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-cyan-400 opacity-75" />
|
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-cyan-500" />
|
|
</span>
|
|
</div>
|
|
<p className="text-[11px] text-slate-400 mt-0.5">
|
|
Real-time OpenZFS ARC caching, storage pool I/O bandwidth, and host telemetry
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex items-center p-1 bg-slate-950 border border-slate-800 rounded-xl text-xs">
|
|
{(['15m', '1h', '6h', '24h'] as const).map((range) => (
|
|
<button
|
|
key={range}
|
|
onClick={() => setTelemetryRange(range)}
|
|
className={`px-2.5 py-1 rounded-lg font-medium transition ${
|
|
telemetryRange === range
|
|
? 'bg-cyan-600 text-white shadow-sm'
|
|
: 'text-slate-400 hover:text-white'
|
|
}`}
|
|
>
|
|
{range}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<div className="text-[11px] font-mono text-cyan-400/80 px-2 py-1 bg-slate-950 rounded-lg border border-slate-800">
|
|
Live Feed
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 4-Panel Telemetry Grid */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
{/* Telemetry 1: ZFS ARC Cache Hit Ratio */}
|
|
<div className="p-4 rounded-xl bg-slate-950/60 border border-slate-800/80 flex flex-col justify-between">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div className="flex items-center gap-1.5 text-xs font-semibold text-slate-200">
|
|
<Zap className="h-3.5 w-3.5 text-cyan-400" />
|
|
<span>ZFS ARC Hit Ratio</span>
|
|
</div>
|
|
<span className="text-xs font-mono font-bold text-cyan-400">98.6%</span>
|
|
</div>
|
|
|
|
<div className="h-28 flex items-end gap-1 pt-3 px-1">
|
|
{arcRatioData.map((val, i) => (
|
|
<div
|
|
key={i}
|
|
className="flex-1 bg-gradient-to-t from-cyan-600/30 to-cyan-400 rounded-t transition-all hover:bg-cyan-300"
|
|
style={{ height: `${(val - 90) * 10}%` }}
|
|
title={`${val}%`}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
<div className="mt-3 pt-2 border-t border-slate-800/60 flex items-center justify-between text-[10px] text-slate-400 font-mono">
|
|
<span>Data: 99.1%</span>
|
|
<span>Metadata: 97.8%</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Telemetry 2: Storage Pool Read/Write Bandwidth */}
|
|
<div className="p-4 rounded-xl bg-slate-950/60 border border-slate-800/80 flex flex-col justify-between">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div className="flex items-center gap-1.5 text-xs font-semibold text-slate-200">
|
|
<HardDrive className="h-3.5 w-3.5 text-blue-400" />
|
|
<span>Pool I/O Throughput</span>
|
|
</div>
|
|
<span className="text-xs font-mono font-bold text-blue-300">142 MB/s Peak</span>
|
|
</div>
|
|
|
|
<div className="h-28 flex items-end gap-1 pt-3 px-1">
|
|
{throughputData.map((val, i) => (
|
|
<div
|
|
key={i}
|
|
className="flex-1 bg-gradient-to-t from-blue-600/30 to-blue-400 rounded-t transition-all hover:bg-blue-300"
|
|
style={{ height: `${(val / 150) * 100}%` }}
|
|
title={`${val} MB/s`}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
<div className="mt-3 pt-2 border-t border-slate-800/60 flex items-center justify-between text-[10px] text-slate-400 font-mono">
|
|
<span>Read: 94 MB/s</span>
|
|
<span>Write: 48 MB/s</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Telemetry 3: CPU Core Utilization */}
|
|
<div className="p-4 rounded-xl bg-slate-950/60 border border-slate-800/80 flex flex-col justify-between">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div className="flex items-center gap-1.5 text-xs font-semibold text-slate-200">
|
|
<Cpu className="h-3.5 w-3.5 text-teal-400" />
|
|
<span>CPU Load & Cores</span>
|
|
</div>
|
|
<span className="text-xs font-mono font-bold text-teal-300">12.4% Avg</span>
|
|
</div>
|
|
|
|
<div className="h-28 flex items-end gap-1 pt-3 px-1">
|
|
{cpuData.map((val, i) => (
|
|
<div
|
|
key={i}
|
|
className="flex-1 bg-gradient-to-t from-teal-600/30 to-teal-400 rounded-t transition-all hover:bg-teal-300"
|
|
style={{ height: `${val * 2.2}%` }}
|
|
title={`${val}%`}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
<div className="mt-3 pt-2 border-t border-slate-800/60 flex items-center justify-between text-[10px] text-slate-400 font-mono">
|
|
<span>4 Cores active</span>
|
|
<span>Load: 0.42, 0.38</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Telemetry 4: Memory & ARC Footprint */}
|
|
<div className="p-4 rounded-xl bg-slate-950/60 border border-slate-800/80 flex flex-col justify-between">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div className="flex items-center gap-1.5 text-xs font-semibold text-slate-200">
|
|
<Layers className="h-3.5 w-3.5 text-indigo-400" />
|
|
<span>RAM & ARC Footprint</span>
|
|
</div>
|
|
<span className="text-xs font-mono font-bold text-slate-300">13.2 / 32.0 GB</span>
|
|
</div>
|
|
|
|
<div className="h-28 flex flex-col justify-center gap-2.5 px-1">
|
|
<div>
|
|
<div className="flex justify-between text-[11px] mb-1">
|
|
<span className="text-slate-400">ZFS ARC Cache</span>
|
|
<span className="font-mono text-cyan-400 font-semibold">4.0 GB (100%)</span>
|
|
</div>
|
|
<div className="w-full bg-slate-800 h-1.5 rounded-full overflow-hidden">
|
|
<div className="bg-cyan-400 h-full rounded-full" style={{ width: '100%' }} />
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="flex justify-between text-[11px] mb-1">
|
|
<span className="text-slate-400">Apps & Workloads</span>
|
|
<span className="font-mono text-blue-400 font-semibold">9.2 GB</span>
|
|
</div>
|
|
<div className="w-full bg-slate-800 h-1.5 rounded-full overflow-hidden">
|
|
<div className="bg-blue-400 h-full rounded-full" style={{ width: '35%' }} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-3 pt-2 border-t border-slate-800/60 flex items-center justify-between text-[10px] text-slate-400 font-mono">
|
|
<span>Free: 18.8 GB</span>
|
|
<span>ZFS ARC Limit: 4 GB</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Homarr-Style Active Services & Workloads Hub */}
|
|
<div className="p-6 rounded-2xl bg-slate-900/70 border border-slate-800">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div className="flex items-center gap-2">
|
|
<Layers className="h-4 w-4 text-cyan-400" />
|
|
<h3 className="text-sm font-semibold text-white">Appliance Services & Workloads</h3>
|
|
<span className="px-2 py-0.5 rounded-full text-[10px] font-semibold bg-cyan-500/10 text-cyan-400 border border-cyan-500/20">
|
|
Native Workload Engine
|
|
</span>
|
|
</div>
|
|
<button
|
|
onClick={() => onNavigate('apps')}
|
|
className="text-xs text-cyan-400 hover:text-cyan-300 font-medium flex items-center gap-1"
|
|
>
|
|
<span>App Store & Catalog</span>
|
|
<ArrowUpRight className="h-3.5 w-3.5" />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
{/* Workload 1: Immich Photo Hub */}
|
|
<div className="p-4 rounded-xl bg-slate-950/70 border border-slate-800/90 hover:border-cyan-500/40 transition-all flex flex-col justify-between group">
|
|
<div>
|
|
<div className="flex items-start justify-between">
|
|
<div className="p-2.5 rounded-xl bg-slate-900 border border-slate-800 shadow-inner group-hover:scale-105 transition-transform">
|
|
<AppIcon name="immich" className="h-8 w-8" />
|
|
</div>
|
|
<div className="flex items-center gap-1.5">
|
|
<span className="h-2 w-2 rounded-full bg-cyan-400 animate-pulse" />
|
|
<span className="text-[10px] font-mono text-cyan-400 font-medium">ONLINE</span>
|
|
</div>
|
|
</div>
|
|
<h4 className="text-sm font-bold text-white mt-3">Immich Photo Hub</h4>
|
|
<p className="text-xs text-slate-400 mt-0.5 line-clamp-2">
|
|
Hardware-accelerated photo & video backup with Intel QuickSync ML
|
|
</p>
|
|
</div>
|
|
<div className="mt-4 pt-3 border-t border-slate-800/80 flex items-center justify-between">
|
|
<span className="text-[11px] font-mono text-slate-400 bg-slate-900 px-2 py-0.5 rounded border border-slate-800">
|
|
:2283
|
|
</span>
|
|
<a
|
|
href="http://naxos.local:2283"
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className="flex items-center gap-1 text-xs text-cyan-400 hover:text-cyan-300 font-medium"
|
|
>
|
|
<span>Launch</span>
|
|
<ArrowUpRight className="h-3 w-3" />
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Workload 2: Jellyfin Media Server */}
|
|
<div className="p-4 rounded-xl bg-slate-950/70 border border-slate-800/90 hover:border-cyan-500/40 transition-all flex flex-col justify-between group">
|
|
<div>
|
|
<div className="flex items-start justify-between">
|
|
<div className="p-2.5 rounded-xl bg-slate-900 border border-slate-800 shadow-inner group-hover:scale-105 transition-transform">
|
|
<AppIcon name="jellyfin" className="h-8 w-8" />
|
|
</div>
|
|
<div className="flex items-center gap-1.5">
|
|
<span className="h-2 w-2 rounded-full bg-cyan-400 animate-pulse" />
|
|
<span className="text-[10px] font-mono text-cyan-400 font-medium">ONLINE</span>
|
|
</div>
|
|
</div>
|
|
<h4 className="text-sm font-bold text-white mt-3">Jellyfin Media Server</h4>
|
|
<p className="text-xs text-slate-400 mt-0.5 line-clamp-2">
|
|
4K HDR media streaming with hardware transcoding & DLNA
|
|
</p>
|
|
</div>
|
|
<div className="mt-4 pt-3 border-t border-slate-800/80 flex items-center justify-between">
|
|
<span className="text-[11px] font-mono text-slate-400 bg-slate-900 px-2 py-0.5 rounded border border-slate-800">
|
|
:8096
|
|
</span>
|
|
<a
|
|
href="http://naxos.local:8096"
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className="flex items-center gap-1 text-xs text-cyan-400 hover:text-cyan-300 font-medium"
|
|
>
|
|
<span>Launch</span>
|
|
<ArrowUpRight className="h-3 w-3" />
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Workload 3: Nextcloud Hub */}
|
|
<div className="p-4 rounded-xl bg-slate-950/70 border border-slate-800/90 hover:border-cyan-500/40 transition-all flex flex-col justify-between group">
|
|
<div>
|
|
<div className="flex items-start justify-between">
|
|
<div className="p-2.5 rounded-xl bg-slate-900 border border-slate-800 shadow-inner group-hover:scale-105 transition-transform">
|
|
<AppIcon name="nextcloud" className="h-8 w-8" />
|
|
</div>
|
|
<div className="flex items-center gap-1.5">
|
|
<span className="h-2 w-2 rounded-full bg-slate-500" />
|
|
<span className="text-[10px] font-mono text-slate-400 font-medium">CATALOG</span>
|
|
</div>
|
|
</div>
|
|
<h4 className="text-sm font-bold text-white mt-3">Nextcloud Hub</h4>
|
|
<p className="text-xs text-slate-400 mt-0.5 line-clamp-2">
|
|
Collaborative office, encrypted file sync, and calendar sharing
|
|
</p>
|
|
</div>
|
|
<div className="mt-4 pt-3 border-t border-slate-800/80 flex items-center justify-between">
|
|
<span className="text-[11px] font-mono text-slate-400 bg-slate-900 px-2 py-0.5 rounded border border-slate-800">
|
|
Ready
|
|
</span>
|
|
<button
|
|
onClick={() => onNavigate('apps')}
|
|
className="flex items-center gap-1 text-xs text-cyan-400 hover:text-cyan-300 font-medium"
|
|
>
|
|
<span>Deploy</span>
|
|
<ArrowUpRight className="h-3 w-3" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Workload 4: Paperless-ngx */}
|
|
<div className="p-4 rounded-xl bg-slate-950/70 border border-slate-800/90 hover:border-cyan-500/40 transition-all flex flex-col justify-between group">
|
|
<div>
|
|
<div className="flex items-start justify-between">
|
|
<div className="p-2.5 rounded-xl bg-slate-900 border border-slate-800 shadow-inner group-hover:scale-105 transition-transform">
|
|
<AppIcon name="paperless" className="h-8 w-8" />
|
|
</div>
|
|
<div className="flex items-center gap-1.5">
|
|
<span className="h-2 w-2 rounded-full bg-slate-500" />
|
|
<span className="text-[10px] font-mono text-slate-400 font-medium">CATALOG</span>
|
|
</div>
|
|
</div>
|
|
<h4 className="text-sm font-bold text-white mt-3">Paperless-ngx</h4>
|
|
<p className="text-xs text-slate-400 mt-0.5 line-clamp-2">
|
|
Automated document archiving with OCR and full-text search
|
|
</p>
|
|
</div>
|
|
<div className="mt-4 pt-3 border-t border-slate-800/80 flex items-center justify-between">
|
|
<span className="text-[11px] font-mono text-slate-400 bg-slate-900 px-2 py-0.5 rounded border border-slate-800">
|
|
Ready
|
|
</span>
|
|
<button
|
|
onClick={() => onNavigate('apps')}
|
|
className="flex items-center gap-1 text-xs text-cyan-400 hover:text-cyan-300 font-medium"
|
|
>
|
|
<span>Deploy</span>
|
|
<ArrowUpRight className="h-3 w-3" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|