'use client'; import { useEffect, useState } from 'react'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { BackupSummaryCards } from '@/components/backup/backup-summary-cards'; import { CompanyBackupTable, CompanyBackupRow } from '@/components/backup/company-backup-table'; import { ComplianceSummaryCards } from '@/components/backup/compliance-summary-cards'; import { ComplianceDetailTable } from '@/components/backup/compliance-detail-table'; import { ContractCoverageTable } from '@/components/backup/contract-coverage-table'; import { RefreshCw, CheckCircle2, AlertTriangle, XCircle, Clock, WifiOff } from 'lucide-react'; import { Skeleton } from '@/components/ui/skeleton'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { StatusBadge } from '@/components/ui/status-badge'; import { RpoJobSummary } from '@/lib/services/veeam-rpo-service'; interface BackupStatusData { totalProtectedWorkloads: number; unprotectedWorkloads: number; successRate24h: number; failedJobs24h: number; warningJobs24h: number; totalBackupServerJobs: number; totalBackupAgentJobs: number; lastSyncAt: string | null; } interface RpoData { summary: { total: number; healthy: number; breached: number; offlineSuppressed: number; withOpenTicket: number; critical: number; high: number; }; jobs: RpoJobSummary[]; } interface OfflineLogRow { id: number; job_name: string; org_name: string; rmm_hostname: string; rmm_site_name: string; device_type_category: string; rmm_last_seen: string | null; hours_offline: number; backup_interval_hours: number; checked_at: string; } interface ComplianceData { summary: { totalContractedDevices: number; matchedDevices: number; contractedNotBackedUp: number; backedUpNotContracted: number; computedAt: string | null; }; mismatches: any[]; } function timeAgo(dateStr: string | null): string { if (!dateStr) return 'Never'; const diff = Date.now() - new Date(dateStr).getTime(); const minutes = Math.floor(diff / 60000); if (minutes < 1) return 'Just now'; if (minutes < 60) return `${minutes}m ago`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h ago`; return `${Math.floor(hours / 24)}d ago`; } function timeAgoHours(hours: number | null): string { if (hours === null) return 'Never'; if (hours < 1) return 'Just now'; if (hours < 24) return `${Math.round(hours)}h ago`; return `${Math.round(hours / 24)}d ago`; } export default function BackupStatusPage() { const [status, setStatus] = useState(null); const [companies, setCompanies] = useState([]); const [compliance, setCompliance] = useState(null); const [rpo, setRpo] = useState(null); const [offlineLog, setOfflineLog] = useState([]); const [loading, setLoading] = useState(true); const [syncing, setSyncing] = useState(false); const fetchData = async () => { try { const [statusRes, companiesRes, complianceRes, rpoRes, offlineLogRes] = await Promise.all([ fetch('/api/veeam/backup-status').then(r => r.json()), fetch('/api/veeam/companies').then(r => r.json()), fetch('/api/veeam/compliance').then(r => r.json()), fetch('/api/veeam/rpo-check').then(r => r.json()), fetch('/api/veeam/rpo-offline-log?limit=200').then(r => r.json()), ]); setStatus(statusRes); setCompanies(Array.isArray(companiesRes) ? companiesRes : []); setCompliance(complianceRes); setRpo(rpoRes); setOfflineLog(offlineLogRes.rows ?? []); } catch (error) { console.error('Failed to fetch backup status:', error); } finally { setLoading(false); } }; useEffect(() => { fetchData(); }, []); const handleSync = async () => { setSyncing(true); try { await fetch('/api/veeam/sync', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ syncType: 'full' }), }); // Poll for completion const poll = setInterval(async () => { const res = await fetch('/api/veeam/sync').then(r => r.json()); if (!res.isSyncing) { clearInterval(poll); setSyncing(false); fetchData(); } }, 3000); // Safety timeout setTimeout(() => { clearInterval(poll); setSyncing(false); fetchData(); }, 120000); } catch { setSyncing(false); } }; const isSyncStale = status?.lastSyncAt ? Date.now() - new Date(status.lastSyncAt).getTime() > 2 * 60 * 60 * 1000 : true; if (loading) { return (
{[...Array(5)].map((_, i) => )}
); } return (
Backup Overview RPO Status {rpo && rpo.summary.breached > 0 && ( {rpo.summary.breached} )} Offline Suppressed {rpo && (rpo.summary.offlineSuppressed ?? 0) > 0 && ( {rpo.summary.offlineSuppressed} )} Contract Compliance {compliance && (compliance.summary.contractedNotBackedUp > 0 || compliance.summary.backedUpNotContracted > 0) && ( {compliance.summary.contractedNotBackedUp + compliance.summary.backedUpNotContracted} )}
{status?.lastSyncAt && ( Last sync: {timeAgo(status.lastSyncAt)} )}
{status && ( )} {rpo && ( <> {/* Summary Cards */}
Healthy Jobs
{rpo.summary.healthy}

of {rpo.summary.total} total

RPO Breached
{rpo.summary.breached}

{rpo.summary.withOpenTicket} with open ticket

Critical
{rpo.summary.critical}

{rpo.summary.high} high priority

Offline Suppressed
{rpo.summary.offlineSuppressed ?? 0}

breached but device offline

Compliance Rate
{rpo.summary.total > 0 ? Math.round((rpo.summary.healthy / rpo.summary.total) * 100) : 0}%

jobs within RPO window

{/* Job Table */}
Job Organization Last Backup RMM Device Status Ticket Failure Reason {rpo.jobs.map((job) => ( {job.job_name} {job.org_name} {timeAgoHours(job.hours_since_backup)} {job.rmm_hostname ? (
{job.rmm_hostname} {job.is_offline_suppressed && (
offline {timeAgo(job.rmm_last_seen)}
)}
) : ( )}
{job.is_offline_suppressed ? ( Offline ) : job.is_breached ? ( Breached ) : ( Healthy )} {job.open_ticket ? ( {job.open_ticket.at_ticket_number} ({job.open_ticket.priority_level}) ) : ( )} {job.failure_category ?? '—'}
))} {rpo.jobs.length === 0 && ( No workstation jobs found )}
)}

Workstation backup jobs suppressed during the last RPO check because the device was offline longer than its backup interval. No Autotask ticket is created while the device is offline.

Device Job Organization Type Last Seen Offline Checked {offlineLog.map((row) => ( {row.rmm_hostname} {row.job_name} {row.org_name} {row.device_type_category} {timeAgo(row.rmm_last_seen)} {row.hours_offline >= 48 ? `${Math.round(row.hours_offline / 24)}d` : `${Math.round(row.hours_offline)}h`} {timeAgo(row.checked_at)} ))} {offlineLog.length === 0 && ( No offline suppressions logged yet )}
{compliance && ( <> Contract Coverage Mismatches {(compliance.summary.contractedNotBackedUp + compliance.summary.backedUpNotContracted) > 0 && ( {compliance.summary.contractedNotBackedUp + compliance.summary.backedUpNotContracted} )} )}
); }