'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 { 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 */}
{rpo.jobs.map((job) => ( ))} {rpo.jobs.length === 0 && ( )}
Job Organization Last Backup RMM Device Status Ticket Failure Reason
{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 ?? '—'}
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.

{offlineLog.map((row) => ( ))} {offlineLog.length === 0 && ( )}
Device Job Organization Type Last Seen Offline Checked
{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)}
No offline suppressions logged yet
{compliance && ( <> Contract Coverage Mismatches {(compliance.summary.contractedNotBackedUp + compliance.summary.backedUpNotContracted) > 0 && ( {compliance.summary.contractedNotBackedUp + compliance.summary.backedUpNotContracted} )} )}
); }