wulf-pulse/components/backup/compliance-detail-table.tsx
lorentz 5dc7a7e66b feat: Veeam VSPC backup integration - sync, compliance, UI
- 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
2026-02-11 21:04:28 -05:00

127 lines
4.2 KiB
TypeScript

'use client';
import { useState } from 'react';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Search } from 'lucide-react';
interface ComplianceMismatch {
id: number;
company_id: number | null;
company_name: string | null;
configuration_item_id: number | null;
veeam_workload_uid: string | null;
mismatch_type: string;
backup_type_udf: string | null;
device_name: string;
contract_name: string | null;
veeam_workload_name: string | null;
}
interface ComplianceDetailTableProps {
mismatches: ComplianceMismatch[];
}
export function ComplianceDetailTable({ mismatches }: ComplianceDetailTableProps) {
const [search, setSearch] = useState('');
const [typeFilter, setTypeFilter] = useState<string>('all');
const filtered = mismatches.filter((m) => {
const matchesSearch =
(m.device_name || '').toLowerCase().includes(search.toLowerCase()) ||
(m.company_name || '').toLowerCase().includes(search.toLowerCase());
const matchesType =
typeFilter === 'all' || m.mismatch_type === typeFilter;
return matchesSearch && matchesType;
});
return (
<div className="space-y-4">
<div className="flex items-center gap-4">
<div className="relative flex-1 max-w-sm">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search devices or companies..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-9"
/>
</div>
<div className="flex gap-2">
{[
{ key: 'all', label: 'All' },
{ key: 'contracted_not_backed_up', label: 'Missing Backup' },
{ key: 'backed_up_not_contracted', label: 'No Contract' },
].map((f) => (
<Button
key={f.key}
variant={typeFilter === f.key ? 'default' : 'outline'}
size="sm"
onClick={() => setTypeFilter(f.key)}
>
{f.label}
</Button>
))}
</div>
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Company</TableHead>
<TableHead>Device</TableHead>
<TableHead>Issue</TableHead>
<TableHead>Backup UDF</TableHead>
<TableHead>Contract</TableHead>
<TableHead>Veeam Workload</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filtered.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="text-center text-muted-foreground py-8">
{mismatches.length === 0
? 'No compliance issues found — all clear!'
: 'No matching results'}
</TableCell>
</TableRow>
) : (
filtered.map((m) => (
<TableRow key={m.id}>
<TableCell className="font-medium">{m.company_name || '-'}</TableCell>
<TableCell>{m.device_name}</TableCell>
<TableCell>
<Badge
className={
m.mismatch_type === 'contracted_not_backed_up'
? 'bg-red-500/10 text-red-500'
: 'bg-yellow-500/10 text-yellow-500'
}
>
{m.mismatch_type === 'contracted_not_backed_up'
? 'Missing Backup'
: 'No Contract'}
</Badge>
</TableCell>
<TableCell className="text-sm">{m.backup_type_udf || '-'}</TableCell>
<TableCell className="text-sm">{m.contract_name || '-'}</TableCell>
<TableCell className="text-sm">{m.veeam_workload_name || '-'}</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</div>
);
}