'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
);
}