diff --git a/src/pages/Monitor.tsx b/src/pages/Monitor.tsx index 2be6887..cd963b8 100644 --- a/src/pages/Monitor.tsx +++ b/src/pages/Monitor.tsx @@ -11,13 +11,17 @@ import { RefreshCw, CheckCircle2, AlertTriangle, - XCircle, Zap, ArrowLeft, Activity, Clock, TrendingUp, Wrench, + Code, + Network, + Cpu, + MemoryStick, + MonitorDot, } from 'lucide-react' // Types @@ -49,21 +53,95 @@ interface MonitorData { } } +interface VMConfig { + name: string + id: string + type: 'QEMU' | 'LXC' + ip: string + role: string + icon: React.ElementType + accent: string + accentBg: string + accentBorder: string +} + +// VM definitions matching cluster architecture +const VM_CONFIG: Record = { + 'CWP Server': { + name: 'Server', + id: 'VM 100', + type: 'QEMU', + ip: '5.9.90.105', + role: 'CWP - hosting clientes', + icon: Server, + accent: 'text-emerald-400', + accentBg: 'bg-emerald-500/10', + accentBorder: 'border-emerald-500/20', + }, + 'EasyPanel': { + name: 'Easy', + id: 'VM 101', + type: 'QEMU', + ip: '5.9.90.70', + role: 'Docker Swarm - 46 servicos', + icon: Container, + accent: 'text-cyan-400', + accentBg: 'bg-cyan-500/10', + accentBorder: 'border-cyan-500/20', + }, + 'Dev': { + name: 'Dev', + id: 'CT 102', + type: 'LXC', + ip: '10.10.10.10', + role: 'Node.js, Docker, TypeScript', + icon: Code, + accent: 'text-violet-400', + accentBg: 'bg-violet-500/10', + accentBorder: 'border-violet-500/20', + }, + 'Gateway': { + name: 'Gateway', + id: 'VM 103', + type: 'QEMU', + ip: '5.9.90.69', + role: '26 MCPs, Nginx proxy', + icon: Network, + accent: 'text-amber-400', + accentBg: 'bg-amber-500/10', + accentBorder: 'border-amber-500/20', + }, +} + // Animation variants const containerVariants = { hidden: { opacity: 0 }, show: { opacity: 1, - transition: { staggerChildren: 0.05 } + transition: { staggerChildren: 0.06 } } } const itemVariants = { hidden: { opacity: 0, y: 20 }, - show: { opacity: 1, y: 0 } + show: { + opacity: 1, + y: 0, + transition: { type: 'spring' as const, stiffness: 300, damping: 30 } + } +} + +// --- Sub-components --- + +const StatusDot = ({ status }: { status: string }) => { + const color = (status === 'ok' || status === 'up') + ? 'bg-emerald-400 shadow-[0_0_12px_rgba(16,185,129,0.6)]' + : status === 'warning' + ? 'bg-amber-400 shadow-[0_0_12px_rgba(245,158,11,0.6)]' + : 'bg-red-400 shadow-[0_0_12px_rgba(239,68,68,0.6)] animate-pulse' + return
} -// Status Badge const StatusBadge = ({ status }: { status: string }) => { const styles: Record = { ok: 'bg-emerald-500/20 text-emerald-400', @@ -80,15 +158,17 @@ const StatusBadge = ({ status }: { status: string }) => { ) } -// Progress Bar -const ProgressBar = ({ percent, showLabel = true }: { percent: number; showLabel?: boolean }) => { - const color = percent < 70 ? 'from-emerald-500 to-emerald-400' : percent < 85 ? 'from-amber-500 to-amber-400' : 'from-red-500 to-red-400' +const ProgressBar = ({ percent, showLabel = true, size = 'md', inverted = false }: { percent: number; showLabel?: boolean; size?: 'sm' | 'md'; inverted?: boolean }) => { + const color = inverted + ? (percent > 90 ? 'from-emerald-500 to-emerald-400' : percent > 70 ? 'from-amber-500 to-amber-400' : 'from-red-500 to-red-400') + : (percent < 70 ? 'from-emerald-500 to-emerald-400' : percent < 85 ? 'from-amber-500 to-amber-400' : 'from-red-500 to-red-400') + const height = size === 'sm' ? 'h-1.5' : 'h-2' return (
-
+
@@ -98,19 +178,13 @@ const ProgressBar = ({ percent, showLabel = true }: { percent: number; showLabel ) } -// Summary Card -const SummaryCard = ({ value, label, color, icon: Icon }: { value: number; label: string; color: string; icon: React.ElementType }) => ( - - -
{value}
-
{label}
-
+const MetricPill = ({ label, value, unit = '%' }: { label: string; value: number | string; unit?: string }) => ( + + {label}{' '} + {value}{unit} + ) -// Category Card const CategoryCard = ({ title, icon: Icon, @@ -136,45 +210,129 @@ const CategoryCard = ({
) -// Monitor Item -const MonitorItemRow = ({ item }: { item: MonitorItem }) => ( -
-
-
{item.name}
- {item.details && ( -
- {item.details.cpu !== undefined && ( - - CPU{' '} - {item.details.cpu}% - - )} - {item.details.ram !== undefined && ( - - RAM{' '} - {item.details.ram}% - - )} - {item.details.disk !== undefined && ( - - Disco{' '} - {item.details.disk}% - - )} - {item.details.response_time !== undefined && ( - {item.details.response_time}s - )} - {item.details.domain && ( - {item.details.domain} - )} +// Cluster Overview Hero +const ClusterHero = ({ data }: { data: MonitorData }) => { + const clusterItem = data.items.server?.find(s => s.name === 'Cluster Proxmox') + const d = clusterItem?.details || {} + + return ( + +
+
+
+ +
+
+

Cluster Proxmox

+

cluster.descomplicar.pt - 5.9.90.75 - Hetzner AX162-R

+
- )} -
- +
+ + + {data.overall === 'ok' ? 'Operacional' : data.overall === 'warning' ? 'Atencao' : 'Critico'} + +
+
+ +
+
+ + + + + + + 0 ? 'text-red-400' : data.stats.total_warning > 0 ? 'text-amber-400' : 'text-emerald-400'} /> +
+
+ + ) +} + +const StatMini = ({ icon: Icon, label, value, sub, color = 'text-white' }: { + icon: React.ElementType; label: string; value: string; sub?: string; color?: string +}) => ( +
+ +
{value}
+
{label}
+ {sub &&
{sub}
}
) -// Main Monitor Page +// VM Card +const VMCard = ({ item, config }: { item?: MonitorItem; config: VMConfig }) => { + const Icon = config.icon + const d = item?.details || {} + const status = item?.status || 'ok' + + return ( + +
+
+
+ +
+
+
+

{config.name}

+ {config.id} +
+

{config.ip} - {config.type}

+
+
+ +
+ +
+

{config.role}

+ + {d.cpu !== undefined && ( +
+
+ CPU + {d.cpu}% +
+ +
+ )} + {d.ram !== undefined && ( +
+
+ RAM + {d.ram}% +
+ +
+ )} + {d.disk !== undefined && ( +
+
+ Disco + {d.disk}% +
+ +
+ )} + {d.load !== undefined && ( +
+ +
+ )} +
+
+ ) +} + +// --- Main Component --- + export default function Monitor() { const [data, setData] = useState(null) const [loading, setLoading] = useState(true) @@ -188,7 +346,9 @@ export default function Monitor() { const json = await response.json() setData(json) } catch { - setData(getMockData()) + if (import.meta.env.DEV) { + setData(getMockData()) + } } finally { setLoading(false) setRefreshing(false) @@ -204,13 +364,9 @@ export default function Monitor() { if (loading) { return (
- + -

A carregar monitorização...

+

A carregar monitorizacao...

) @@ -218,8 +374,10 @@ export default function Monitor() { if (!data) return null - // Items já vêm agrupados por categoria da API - const groupedItems = data.items + const { items } = data + + // Match server items to VM config + const getVMItem = (name: string) => items.server?.find(s => s.name === name) const overallColor = { ok: 'text-emerald-400 bg-emerald-500/20', @@ -229,8 +387,8 @@ export default function Monitor() { const overallLabel = { ok: 'Operacional', - warning: 'Atenção', - critical: 'Crítico', + warning: 'Atencao', + critical: 'Critico', }[data.overall] return ( @@ -248,8 +406,8 @@ export default function Monitor() {
-

Monitorização

-

Sistemas Descomplicar

+

Monitorizacao

+

Cluster Proxmox Descomplicar

@@ -287,34 +445,46 @@ export default function Monitor() { {/* Main Content */}
- - {/* Summary Grid */} -
- - - + + + {/* Section 1: Cluster Overview */} + + + {/* Section 2: VM Grid */} +
+ {Object.entries(VM_CONFIG).map(([key, config]) => ( + + ))}
- {/* Monitor Grid */} -
- {/* Servers */} - {groupedItems.server && ( - - {groupedItems.server.map((item) => ( - + {/* Section 3: Detail Categories */} +
+ + {/* Sites WordPress */} + {items.site && items.site.length > 0 && ( + + {items.site.map((item) => ( +
+
+
{item.name}
+
+ {item.details?.domain && {item.details.domain}} + {item.details?.response_time !== undefined && ( + {item.details.response_time}s + )} +
+
+ +
))}
)} - {/* Services - Compact Grid */} - {groupedItems.service && ( - + {/* Servicos EasyPanel */} + {items.service && items.service.length > 0 && ( +
- {groupedItems.service.map((item) => ( + {items.service.map((item) => (
)} - {/* Sites */} - {groupedItems.site && ( - - {groupedItems.site.map((item) => ( - - ))} - - )} - - {/* Containers */} - {groupedItems.container && groupedItems.container[0] && ( - -
+ {/* Containers EasyPanel */} + {items.container && items.container[0] && ( + +
- {groupedItems.container[0].details?.up || 0} + {items.container[0].details?.up || 0}
- de {groupedItems.container[0].details?.total || 0} containers activos + de {items.container[0].details?.total || 0} containers activos
- {(groupedItems.container[0].details?.down || 0) > 0 && ( -
- {groupedItems.container[0].details.down} containers em baixo + {(items.container[0].details?.down || 0) > 0 && ( +
+ {items.container[0].details.down} em baixo
)}
)} - {/* Backups */} - {groupedItems.backup && ( - - {groupedItems.backup.map((item) => ( + {/* Backups - 3 camadas */} + {items.backup && items.backup.length > 0 && ( + + {items.backup.map((item) => (
{item.name}
{item.details?.age_hours !== undefined && ( -
Último: há {item.details.age_hours}h
+
+ {item.details.age_hours < 1 ? 'Agora' : + item.details.age_hours < 24 ? `ha ${item.details.age_hours}h` : + `ha ${Math.floor(item.details.age_hours / 24)}d`} +
)}
@@ -379,9 +545,9 @@ export default function Monitor() { )} {/* Storage */} - {groupedItems.storage && ( - - {groupedItems.storage.map((item) => ( + {items.storage && items.storage.length > 0 && ( + + {items.storage.map((item) => (
{item.name} @@ -395,21 +561,20 @@ export default function Monitor() { )} - {/* Maintenance */} - {groupedItems.maintenance && groupedItems.maintenance[0] && (() => { - const m = groupedItems.maintenance[0] + {/* Manutencao */} + {items.maintenance && items.maintenance[0] && (() => { + const m = items.maintenance[0] const d = m.details || {} const ageH = d.age_hours ?? 0 - const ageDays = Math.floor(ageH / 24) - const ageLabel = ageH < 24 ? `há ${ageH}h` : `há ${ageDays}d` + const ageLabel = ageH < 1 ? 'agora' : ageH < 24 ? `ha ${ageH}h` : `ha ${Math.floor(ageH / 24)}d` const actions = (d.logs_truncated || 0) + (d.images_removed || 0) + (d.orphan_volumes || 0) + (d.tmp_cleaned || 0) return ( - +
Auto-Cleanup
-
Último: {ageLabel}
+
Ultimo: {ageLabel}
@@ -436,34 +601,62 @@ export default function Monitor() { ) })()} - {/* WP Updates */} - {groupedItems.wp_update && groupedItems.wp_update[0] && ( - -
- {(groupedItems.wp_update[0].details?.manual_updates || 0) > 0 ? ( + {/* WP Updates - per-site detail from 'site' category */} + {(() => { + const sites = items.site || [] + const sitesWithUpdates = sites.filter((s: MonitorItem) => { + const counts = s.details?.updates?.counts + return counts && counts.total > 0 + }) + const totalUpdates = sitesWithUpdates.reduce((sum: number, s: MonitorItem) => sum + (s.details?.updates?.counts?.total || 0), 0) + const wpAgg = items.wp_update?.[0] + + return ( + 0 ? sitesWithUpdates.length : undefined}> + {sitesWithUpdates.length > 0 ? ( <> -
- {groupedItems.wp_update[0].details.manual_updates} -
-
- plugins precisam update manual +
+
{totalUpdates}
+
updates em {sitesWithUpdates.length} sites
+ {sitesWithUpdates.map((site: MonitorItem) => { + const counts = site.details?.updates?.counts || {} + return ( +
+
+ {site.name} + {counts.total} updates +
+
+ {counts.plugins > 0 && {counts.plugins} plugins} + {counts.themes > 0 && {counts.themes} temas} + {(counts.core || 0) > 0 && {counts.core} core} +
+
+ ) + })} + ) : wpAgg && (wpAgg.details?.manual_updates || 0) > 0 ? ( +
+
{wpAgg.details.manual_updates}
+
plugins precisam update
+
Sem detalhe por site disponivel
+
) : ( - <> +
Tudo actualizado
- +
)} -
-
- )} + + ) + })()}
{/* Footer */}
- Última actualização: {new Date().toLocaleTimeString('pt-PT')} + Ultima actualizacao: {new Date().toLocaleTimeString('pt-PT')} · Auto-refresh: 60s
@@ -474,74 +667,74 @@ export default function Monitor() { ) } -// Mock data +// Mock data reflecting new cluster architecture function getMockData(): MonitorData { const mockItems: Record = { server: [ - { id: 5, name: 'CWP Server', category: 'server', status: 'up', details: { cpu: 7.3, ram: 10.2, disk: 39 }, last_check: '' }, - { id: 6, name: 'EasyPanel', category: 'server', status: 'up', details: { cpu: 20.5, ram: 20.2, disk: 41 }, last_check: '' }, - { id: 296, name: 'MCP Hub', category: 'server', status: 'up', details: { cpu: 1.0, load: 0.0 }, last_check: '' }, - { id: 297, name: 'Meet', category: 'server', status: 'up', details: { cpu: 4.7, load: 0.0 }, last_check: '' }, - { id: 298, name: 'WhatsApp', category: 'server', status: 'up', details: { cpu: 3.0, load: 0.04 }, last_check: '' }, - { id: 299, name: 'WhatSMS', category: 'server', status: 'up', details: { cpu: 2.1, load: 0.08 }, last_check: '' }, + { id: 1, name: 'Cluster Proxmox', category: 'server', status: 'ok', details: { cpu: 12.5 }, last_check: '' }, + { id: 100, name: 'CWP Server', category: 'server', status: 'ok', details: { cpu: 7.3, ram: 25.1, disk: 39, load: 0.42 }, last_check: '' }, + { id: 101, name: 'EasyPanel', category: 'server', status: 'ok', details: { cpu: 20.5, ram: 53.1, disk: 41, load: 1.2 }, last_check: '' }, + { id: 102, name: 'Dev', category: 'server', status: 'ok', details: { cpu: 3.0, ram: 18.5, disk: 28 }, last_check: '' }, + { id: 103, name: 'Gateway', category: 'server', status: 'ok', details: { cpu: 5.2, ram: 42.0, disk: 52, load: 0.3 }, last_check: '' }, ], service: [ - { id: 1, name: 'Planeamento EAL', category: 'service', status: 'up', details: {}, last_check: '' }, - { id: 2, name: 'Desk CRM', category: 'service', status: 'up', details: {}, last_check: '' }, - { id: 3, name: 'Automator N8N', category: 'service', status: 'up', details: {}, last_check: '' }, - { id: 4, name: 'NextCloud', category: 'service', status: 'up', details: {}, last_check: '' }, - { id: 517, name: 'Gitea', category: 'service', status: 'up', details: {}, last_check: '' }, - { id: 518, name: 'Meet Jitsi', category: 'service', status: 'up', details: {}, last_check: '' }, - { id: 519, name: 'WikiJS', category: 'service', status: 'up', details: {}, last_check: '' }, - { id: 521, name: 'Google Docs', category: 'service', status: 'up', details: {}, last_check: '' }, - { id: 549, name: 'MCP Hub', category: 'service', status: 'up', details: {}, last_check: '' }, - { id: 515, name: 'WhatSMS', category: 'service', status: 'up', details: {}, last_check: '' }, - { id: 41594, name: 'Syncthing', category: 'service', status: 'up', details: {}, last_check: '' }, + { id: 1, name: 'Desk CRM', category: 'service', status: 'up', details: {}, last_check: '' }, + { id: 2, name: 'N8N', category: 'service', status: 'up', details: {}, last_check: '' }, + { id: 3, name: 'Gitea', category: 'service', status: 'up', details: {}, last_check: '' }, + { id: 4, name: 'WikiJS', category: 'service', status: 'up', details: {}, last_check: '' }, + { id: 5, name: 'Authentik', category: 'service', status: 'up', details: {}, last_check: '' }, + { id: 6, name: 'Metabase', category: 'service', status: 'up', details: {}, last_check: '' }, + { id: 7, name: 'NextCloud', category: 'service', status: 'up', details: {}, last_check: '' }, + { id: 8, name: 'Syncthing', category: 'service', status: 'up', details: {}, last_check: '' }, + { id: 9, name: 'MCP Gateway', category: 'service', status: 'up', details: {}, last_check: '' }, + { id: 10, name: 'Outline', category: 'service', status: 'up', details: {}, last_check: '' }, + { id: 11, name: 'Penpot', category: 'service', status: 'warning', details: {}, last_check: '' }, + { id: 12, name: 'WhatSMS', category: 'service', status: 'up', details: {}, last_check: '' }, ], site: [ - { id: 15960, name: 'Descomplicar', category: 'site', status: 'up', details: { domain: 'descomplicar.pt', response_time: 0.093 }, last_check: '' }, - { id: 15961, name: 'Emanuel Almeida', category: 'site', status: 'up', details: { domain: 'emanuelalmeida.pt', response_time: 1.687 }, last_check: '' }, - { id: 15959, name: 'Family Clinic', category: 'site', status: 'up', details: { domain: 'familyclinic.pt', response_time: 2.712 }, last_check: '' }, - { id: 15962, name: 'WTC', category: 'site', status: 'up', details: { domain: 'wtc-group.com' }, last_check: '' }, - { id: 15963, name: 'Carstuff', category: 'site', status: 'down', details: { domain: 'carstuff.pt' }, last_check: '' }, - { id: 15964, name: 'Espiral Senior', category: 'site', status: 'up', details: { domain: 'espiralsenior.pt' }, last_check: '' }, - { id: 15965, name: 'Karate Gaia', category: 'site', status: 'up', details: { domain: 'karategaia.pt' }, last_check: '' }, + { id: 1, name: 'Descomplicar', category: 'site', status: 'up', details: { domain: 'descomplicar.pt', response_time: 0.093 }, last_check: '' }, + { id: 2, name: 'Emanuel Almeida', category: 'site', status: 'up', details: { domain: 'emanuelalmeida.pt', response_time: 1.687 }, last_check: '' }, + { id: 3, name: 'Family Clinic', category: 'site', status: 'up', details: { domain: 'familyclinic.pt', response_time: 2.712 }, last_check: '' }, + { id: 4, name: 'WTC', category: 'site', status: 'up', details: { domain: 'wtc-group.com' }, last_check: '' }, + { id: 5, name: 'Carstuff', category: 'site', status: 'down', details: { domain: 'carstuff.pt' }, last_check: '' }, + { id: 6, name: 'Espiral Senior', category: 'site', status: 'up', details: { domain: 'espiralsenior.pt' }, last_check: '' }, + { id: 7, name: 'Karate Gaia', category: 'site', status: 'up', details: { domain: 'karategaia.pt' }, last_check: '' }, ], container: [ - { id: 7, name: 'EasyPanel Containers', category: 'container', status: 'warning', details: { up: 83, total: 87, down: 4 }, last_check: '' }, + { id: 1, name: 'Docker Swarm', category: 'container', status: 'ok', details: { up: 44, total: 46, down: 2 }, last_check: '' }, ], backup: [ - { id: 15967, name: 'MySQL Hourly', category: 'backup', status: 'ok', details: { age_hours: 1 }, last_check: '' }, - { id: 15968, name: 'CWP Accounts', category: 'backup', status: 'warning', details: { age_hours: 48 }, last_check: '' }, - { id: 15969, name: 'Easy Backup', category: 'backup', status: 'ok', details: { age_hours: 12 }, last_check: '' }, - { id: 15970, name: 'Server->Easy Sync', category: 'backup', status: 'failed', details: { age_hours: 72 }, last_check: '' }, + { id: 1, name: 'Proxmox vzdump', category: 'backup', status: 'ok', details: { age_hours: 8 }, last_check: '' }, + { id: 2, name: 'CWP Accounts', category: 'backup', status: 'ok', details: { age_hours: 18 }, last_check: '' }, + { id: 3, name: 'MySQL Hourly', category: 'backup', status: 'ok', details: { age_hours: 1 }, last_check: '' }, ], storage: [ - { id: 15971, name: 'gordo', category: 'storage', status: 'ok', details: { used: '89GB', total: '200GB', percent: 45 }, last_check: '' }, - { id: 15972, name: 'gordito', category: 'storage', status: 'ok', details: { used: '42GB', total: '100GB', percent: 42 }, last_check: '' }, - ], - wp_update: [ - { id: 25, name: 'WordPress Plugins', category: 'wp_update', status: 'warning', details: { manual_updates: 3 }, last_check: '' }, + { id: 1, name: 'NVMe RAID1 (vg0)', category: 'storage', status: 'ok', details: { used: '650GB', total: '950GB', percent: 68 }, last_check: '' }, + { id: 2, name: 'HDD pbs-local', category: 'storage', status: 'ok', details: { used: '2.9TB', total: '6TB', percent: 49 }, last_check: '' }, + { id: 3, name: 'HDD vm-storage', category: 'storage', status: 'ok', details: { used: '480GB', total: '8.5TB', percent: 6 }, last_check: '' }, ], maintenance: [ - { id: 48558, name: 'EasyPanel Cleanup', category: 'maintenance', status: 'ok', details: { age_hours: 0, disk_percent: 15, freed_mb: 0, logs_truncated: 0, images_removed: 0, orphan_volumes: 0, tmp_cleaned: 0 }, last_check: '' }, + { id: 1, name: 'EasyPanel Cleanup', category: 'maintenance', status: 'ok', details: { age_hours: 2, disk_percent: 41, freed_mb: 128, logs_truncated: 3, images_removed: 5, orphan_volumes: 0, tmp_cleaned: 1 }, last_check: '' }, + ], + wp_update: [ + { id: 1, name: 'WordPress Plugins', category: 'wp_update', status: 'ok', details: { manual_updates: 0 }, last_check: '' }, ], } return { overall: 'warning', summary: [ - { category: 'server', total: 6, ok: 6, warning: 0, critical: 0 }, - { category: 'service', total: 11, ok: 11, warning: 0, critical: 0 }, + { category: 'server', total: 5, ok: 5, warning: 0, critical: 0 }, + { category: 'service', total: 12, ok: 11, warning: 1, critical: 0 }, { category: 'site', total: 7, ok: 6, warning: 0, critical: 1 }, - { category: 'container', total: 1, ok: 0, warning: 1, critical: 0 }, - { category: 'backup', total: 4, ok: 2, warning: 1, critical: 1 }, - { category: 'storage', total: 2, ok: 2, warning: 0, critical: 0 }, + { category: 'container', total: 1, ok: 1, warning: 0, critical: 0 }, + { category: 'backup', total: 3, ok: 3, warning: 0, critical: 0 }, + { category: 'storage', total: 3, ok: 3, warning: 0, critical: 0 }, ], stats: { - total_ok: 26, - total_warning: 2, - total_critical: 2 + total_ok: 30, + total_warning: 1, + total_critical: 1 }, items: mockItems, }