'use client'; import { useState, useEffect } from 'react'; import Link from 'next/link'; import { Button } from '@/components/ui/button'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { ArrowLeft, Activity, History, Calendar, Shield, RefreshCw, Loader2, CheckCircle2, XCircle, AlertTriangle, Clock, Server, HardDrive, Bot, Bell, ChevronDown, ChevronRight, Target, Play, } from 'lucide-react'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { StatusBadge } from '@/components/ui/status-badge'; import SyncScheduler from '@/components/admin/SyncScheduler'; // ── Helpers ─────────────────────────────────────────────────────────────────── function fmtDate(d: string | null | undefined) { if (!d) return 'Never'; return new Date(d).toLocaleString(undefined, { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' }); } function fmtDur(ms: number) { if (ms < 60000) return `${Math.round(ms / 1000)}s`; return `${Math.floor(ms / 60000)}m ${Math.round((ms % 60000) / 1000)}s`; } function StatCard({ label, value, sub, icon: Icon, cls }: { label: string; value: string | number; sub?: string; icon?: React.ElementType; cls?: string; }) { return (
{Icon && }{label}
{value}
{sub &&
{sub}
}
); } function SyncStatusBadge({ status }: { status: string }) { const tone = status === 'completed' ? 'ok' : status === 'failed' ? 'error' : status === 'started' ? 'info' : 'warn'; return {status}; } // ── Status Tab ──────────────────────────────────────────────────────────────── function VeeamStatusTab({ data, onSync, syncing }: { data: any; onSync: (t: string) => void; syncing: boolean }) { if (!data) return (
); const aj = data.agentJobs ?? {}; const bj = data.backupJobs ?? {}; const totalFailed = (aj.failed ?? 0) + (bj.failed ?? 0); const totalWarning = (aj.warning ?? 0) + (bj.warning ?? 0); const totalRunning = aj.running ?? 0; const totalSuccess = (aj.success ?? 0) + (bj.success ?? 0); return (

{data.configured ? 'Connected to VSPC' : 'Not configured'}

Last sync: {fmtDate(data.lastSync)}

0 ? 'border-blue-500/30 bg-blue-500/5' : ''} /> 0 ? 'border-red-500/30 bg-red-500/5' : ''} /> 0 ? 'border-yellow-500/30 bg-yellow-500/5' : ''} />
{(totalFailed > 0 || totalWarning > 0 || totalRunning > 0) && (

Attention Required

{totalFailed > 0 &&
{totalFailed} job{totalFailed !== 1 ? 's' : ''} failed
} {totalWarning > 0 &&
{totalWarning} job{totalWarning !== 1 ? 's' : ''} with warnings
} {totalRunning > 0 &&
{totalRunning} job{totalRunning !== 1 ? 's' : ''} running
}
)}
); } // ── History Tab ─────────────────────────────────────────────────────────────── const ENTITY_LABELS: Record = { organizations: 'Organizations', backup_servers: 'Backup Servers', repositories: 'Repositories', backup_jobs: 'Backup Jobs', backup_agent_jobs: 'Agent Jobs', protected_workloads:'Protected Workloads', backup_agents: 'Agents', alarms: 'Alarms', }; function HistoryRow({ row }: { row: any }) { const [expanded, setExpanded] = useState(false); const dur = row.completed_at && row.started_at ? new Date(row.completed_at).getTime() - new Date(row.started_at).getTime() : null; let entities: Array<{ entity: string; success: boolean; recordsUpserted: number; duration: number; error?: string }> = []; try { if (row.entity_details) { entities = typeof row.entity_details === 'string' ? JSON.parse(row.entity_details) : row.entity_details; } } catch { /* ignore parse errors */ } return ( <> 0 ? 'cursor-pointer' : ''} onClick={() => entities.length > 0 && setExpanded(e => !e)} >
{entities.length > 0 ? (expanded ? : ) : } {row.sync_type}
{(row.records_added ?? 0).toLocaleString()} {fmtDate(row.started_at)} {dur != null ? fmtDur(dur) : '—'} {row.triggered_by ?? '—'}
{expanded && entities.length > 0 && (

Entity Breakdown

{entities.map((e) => (
{ENTITY_LABELS[e.entity] ?? e.entity}
{e.success ? <>{e.recordsUpserted.toLocaleString()} records · {fmtDur(e.duration)} : Failed: {e.error?.substring(0, 60)}}
))}
{row.error_message && (
{row.error_message}
)}
)} ); } function VeeamHistoryTab({ refreshKey }: { refreshKey: number }) { const [rows, setRows] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { setLoading(true); fetch('/api/sync/history?entityType=veeam&limit=50') .then(r => r.json()) .then(d => setRows(d.history ?? [])) .catch(() => setRows([])) .finally(() => setLoading(false)); }, [refreshKey]); if (loading) return
; if (!rows.length) return
No sync history yet — run a sync to populate
; return (
Type Status Records Started Duration Triggered By {rows.map((row, i) => )}
); } // ── Agents Tab ──────────────────────────────────────────────────────────────── function AgentsTab({ refreshKey }: { refreshKey: number }) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { setLoading(true); fetch('/api/veeam/agents') .then(r => r.json()) .then(d => setData(d)) .catch(() => setData(null)) .finally(() => setLoading(false)); }, [refreshKey]); if (loading) return
; if (!data || !data.agents?.length) return
No agent data — run a sync first
; const s = data.summary ?? {}; const agents: any[] = data.agents ?? []; return (
0 ? 'border-red-500/30 bg-red-500/5' : ''} /> 0 ? 'border-yellow-500/30 bg-yellow-500/5' : ''} />
Name Organization Platform Status Agent Status Version Mode Jobs {agents.map((a: any) => ( {a.name} {a.organization_name ?? '—'} {a.agent_platform ?? '—'} {a.status ?? '—'} {a.management_agent_status ?? '—'} {a.version ?? '—'} {a.version_status === 'Outdated' && ' ⚠'} {a.operation_mode ?? '—'} {a.success_jobs_count ?? 0}✓ {(a.running_jobs_count ?? 0) > 0 && {a.running_jobs_count}▶} {(a.total_jobs_count ?? 0) - (a.success_jobs_count ?? 0) - (a.running_jobs_count ?? 0) > 0 && ( {(a.total_jobs_count ?? 0) - (a.success_jobs_count ?? 0) - (a.running_jobs_count ?? 0)}✗ )} ))}
); } // ── Alarms Tab ──────────────────────────────────────────────────────────────── function AlarmsTab({ refreshKey }: { refreshKey: number }) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { setLoading(true); fetch('/api/veeam/alarms') .then(r => r.json()) .then(d => setData(d)) .catch(() => setData(null)) .finally(() => setLoading(false)); }, [refreshKey]); if (loading) return
; if (!data || !data.alarms?.length) return
No alarm data — run a sync first
; const s = data.summary ?? {}; const alarms: any[] = data.alarms ?? []; return (
0 ? 'border-red-500/30 bg-red-500/5' : ''} /> 0 ? 'border-yellow-500/30 bg-yellow-500/5' : ''} />
Object Type Organization Status Repeats Last Activation Message {alarms.map((a: any) => ( {a.object_computer_name || a.object_name || '—'} {a.object_type ?? '—'} {a.organization_name ?? '—'} {a.last_activation_status ?? '—'} {a.repeat_count ?? 0} {fmtDate(a.last_activation_time)} {a.last_activation_message?.trim() || '—'} ))}
); } // ── RPO Tab ─────────────────────────────────────────────────────────────────── function RpoTab({ refreshKey }: { refreshKey: number }) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [running, setRunning] = useState(false); const [lastResult, setLastResult] = useState(null); const fetchStatus = () => { setLoading(true); fetch('/api/veeam/rpo-check') .then(r => r.json()) .then(d => setData(d)) .catch(() => setData(null)) .finally(() => setLoading(false)); }; useEffect(() => { fetchStatus(); }, [refreshKey]); const runCheck = async () => { setRunning(true); try { const r = await fetch('/api/veeam/rpo-check', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) }); const d = await r.json(); setLastResult(d); fetchStatus(); } catch { /* ignore */ } finally { setRunning(false); } }; if (loading) return
; const s = data?.summary ?? {}; const jobs: any[] = data?.jobs ?? []; const breachedJobs = jobs.filter((j: any) => j.is_breached); const healthyJobs = jobs.filter((j: any) => !j.is_breached); return (

RPO-Based Workstation Backup Alerting

One ticket per job — created when RPO is breached, auto-resolved when backup succeeds. Enable the scheduler to run every 30 min.

{lastResult && !lastResult.error && (

Last Run Result

Checked: {lastResult.checked} New Tickets: {lastResult.newTickets} Escalated: {lastResult.escalated} Resolved: {lastResult.resolved} {lastResult.errors?.length > 0 && Errors: {lastResult.errors.length}}
)}
0 ? 'border-red-500/30 bg-red-500/5' : ''} /> 0 ? 'border-yellow-500/30 bg-yellow-500/5' : ''} /> 0 ? 'border-red-500/30 bg-red-500/5' : ''} />
{breachedJobs.length > 0 && (

RPO Breached ({breachedJobs.length})

Job Organization Last Backup Overdue Failure Reason Ticket {breachedJobs.map((j: any) => { const hrs = j.hours_since_backup; const display = hrs === null ? 'Never' : hrs >= 48 ? `${Math.round(hrs / 24)}d` : `${Math.round(hrs)}h`; const ticketTone = j.open_ticket?.priority_level === 'critical' ? 'error' : j.open_ticket?.priority_level === 'high' ? 'warn' : 'pending'; return ( {j.job_name} {j.org_name} {fmtDate(j.last_end_time)} {display} {j.failure_category ?? '—'} {j.open_ticket ? ( {j.open_ticket.at_ticket_number} · {j.open_ticket.priority_level} ) : ( No ticket yet )} ); })}
)} {healthyJobs.length > 0 && (
Within RPO ({healthyJobs.length}) Job Organization Last Backup Hours Ago RPO {healthyJobs.map((j: any) => ( {j.job_name} {j.org_name} {fmtDate(j.last_end_time)} {j.hours_since_backup !== null ? `${j.hours_since_backup}h` : '—'} {j.rpo_hours}h ))}
)}
); } // ── Page ────────────────────────────────────────────────────────────────────── export default function VeeamSyncPage() { const [status, setStatus] = useState(null); const [syncing, setSyncing] = useState(false); const [refreshKey, setRefreshKey] = useState(0); const fetchStatus = async () => { const res = await fetch('/api/integrations/status'); if (res.ok) { const d = await res.json(); setStatus(d.veeam); } }; useEffect(() => { fetchStatus(); }, [refreshKey]); const handleSync = async (syncType = 'full') => { setSyncing(true); try { await fetch('/api/veeam/sync', { method: 'POST', body: JSON.stringify({ syncType }), headers: { 'Content-Type': 'application/json' }, }); const poll = setInterval(async () => { try { const r = await fetch('/api/veeam/sync'); if (r.ok) { const d = await r.json(); if (!d.isSyncing) { clearInterval(poll); setSyncing(false); setRefreshKey(k => k + 1); } } } catch { /* keep polling */ } }, 4000); } catch { setSyncing(false); } }; return (

Backup — Veeam VSPC

Agent jobs, backup jobs, protected workloads, agents, alarms

Status RPO History Agents Alarms Schedules
); }