- Database: 7 Veeam tables + backup_type_udf column on configuration_items - API Client: VSPC REST API v3 client with pagination, rate limiting, Bearer auth - Sync Service: full/incremental sync for orgs, servers, repos, jobs, agent jobs, workloads - Scheduler: veeam-incremental (30min) and veeam-full (daily 2AM) schedules - Compliance Engine: cross-references Autotask config items vs Veeam workloads - API Endpoints: backup-status, companies, workloads, jobs, repos, compliance, sync - UI: Backup Status page with Overview + Contract Compliance tabs - Navigation: added Backup Status link with HardDrive icon - Docker: added VEEAM_VSPC_URL and VEEAM_VSPC_API_KEY env vars to compose
66 lines
1.9 KiB
TypeScript
66 lines
1.9 KiB
TypeScript
'use client';
|
|
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { FileCheck, CheckCircle, XCircle, AlertTriangle } from 'lucide-react';
|
|
|
|
interface ComplianceSummaryCardsProps {
|
|
totalContractedDevices: number;
|
|
matchedDevices: number;
|
|
contractedNotBackedUp: number;
|
|
backedUpNotContracted: number;
|
|
}
|
|
|
|
export function ComplianceSummaryCards({
|
|
totalContractedDevices,
|
|
matchedDevices,
|
|
contractedNotBackedUp,
|
|
backedUpNotContracted,
|
|
}: ComplianceSummaryCardsProps) {
|
|
const cards = [
|
|
{
|
|
title: 'Contracted Backups',
|
|
value: totalContractedDevices,
|
|
icon: FileCheck,
|
|
color: 'text-blue-500',
|
|
},
|
|
{
|
|
title: 'Matched',
|
|
value: matchedDevices,
|
|
icon: CheckCircle,
|
|
color: 'text-green-500',
|
|
},
|
|
{
|
|
title: 'Missing Backup',
|
|
value: contractedNotBackedUp,
|
|
icon: XCircle,
|
|
color: contractedNotBackedUp > 0 ? 'text-red-500' : 'text-green-500',
|
|
subtitle: 'Contracted but not in Veeam',
|
|
},
|
|
{
|
|
title: 'No Contract',
|
|
value: backedUpNotContracted,
|
|
icon: AlertTriangle,
|
|
color: backedUpNotContracted > 0 ? 'text-yellow-500' : 'text-green-500',
|
|
subtitle: 'In Veeam but no contract',
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
{cards.map((card) => (
|
|
<Card key={card.title}>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">{card.title}</CardTitle>
|
|
<card.icon className={`h-4 w-4 ${card.color}`} />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">{card.value}</div>
|
|
{card.subtitle && (
|
|
<p className="text-xs text-muted-foreground mt-1">{card.subtitle}</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|