'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,
} from 'lucide-react';
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 StatusBadge({ status }: { status: string }) {
const cls =
status === 'completed' ? 'bg-green-500/15 text-green-700' :
status === 'failed' ? 'bg-red-500/15 text-red-600' :
status === 'started' ? 'bg-blue-500/15 text-blue-700' :
'bg-yellow-500/15 text-yellow-700';
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() || '—'}
|
))}
);
}
// ── 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
History
Agents
Alarms
Schedules
);
}