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
|
|
|
import { NextResponse } from 'next/server';
|
|
|
|
|
import postgresClient from '@/lib/services/postgres-client';
|
|
|
|
|
|
|
|
|
|
export async function GET() {
|
|
|
|
|
try {
|
|
|
|
|
// Get compliance summary
|
|
|
|
|
const summaryResult = await postgresClient.query(`
|
|
|
|
|
SELECT
|
|
|
|
|
mismatch_type,
|
|
|
|
|
COUNT(*) as count
|
|
|
|
|
FROM veeam_compliance_results
|
|
|
|
|
GROUP BY mismatch_type
|
|
|
|
|
`);
|
|
|
|
|
|
|
|
|
|
let contractedNotBackedUp = 0;
|
|
|
|
|
let backedUpNotContracted = 0;
|
|
|
|
|
for (const row of summaryResult.rows) {
|
|
|
|
|
if (row.mismatch_type === 'contracted_not_backed_up') {
|
|
|
|
|
contractedNotBackedUp = parseInt(row.count);
|
|
|
|
|
} else if (row.mismatch_type === 'backed_up_not_contracted') {
|
|
|
|
|
backedUpNotContracted = parseInt(row.count);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get total contracted devices (config items with backup UDF + company has active contract)
|
|
|
|
|
const contractedResult = await postgresClient.query(`
|
|
|
|
|
SELECT COUNT(DISTINCT ci.id) as count
|
|
|
|
|
FROM configuration_items ci
|
|
|
|
|
JOIN contracts ct ON ct.company_id = ci.company_id AND ct.status = 1
|
|
|
|
|
WHERE ci.backup_type_udf IS NOT NULL
|
|
|
|
|
AND ci.backup_type_udf != ''
|
|
|
|
|
AND ci.is_active = true
|
|
|
|
|
AND ci.is_deleted = false
|
|
|
|
|
`);
|
|
|
|
|
const totalContracted = parseInt(contractedResult.rows[0]?.count || '0');
|
|
|
|
|
const matched = totalContracted - contractedNotBackedUp;
|
|
|
|
|
|
|
|
|
|
// Get last computed time
|
|
|
|
|
const lastComputed = await postgresClient.query(
|
|
|
|
|
'SELECT MAX(computed_at) as computed_at FROM veeam_compliance_results'
|
|
|
|
|
);
|
|
|
|
|
|
2026-03-11 09:34:51 -04:00
|
|
|
// Get mismatch details with active contract service coverage.
|
|
|
|
|
// "Covered" = company has an active contract with a ContractService line
|
|
|
|
|
// matching workstation backup (service_name ILIKE '%workstation%backup%' or '%w/ backup%').
|
|
|
|
|
// Falls back to billing_items if contract_services not yet populated.
|
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
|
|
|
const mismatches = await postgresClient.query(`
|
2026-03-11 09:34:51 -04:00
|
|
|
WITH cs_backup AS (
|
|
|
|
|
SELECT DISTINCT ON (ct.company_id)
|
|
|
|
|
ct.company_id,
|
|
|
|
|
ct.id AS contract_id,
|
|
|
|
|
ct.contract_name,
|
|
|
|
|
cs.quantity AS contracted_qty
|
|
|
|
|
FROM contract_services cs
|
|
|
|
|
JOIN contracts ct ON ct.id = cs.contract_id
|
|
|
|
|
WHERE cs.is_deleted = false
|
|
|
|
|
AND ct.is_deleted = false
|
|
|
|
|
AND ct.status = 1
|
|
|
|
|
AND (
|
|
|
|
|
cs.service_name ILIKE '%workstation%backup%'
|
|
|
|
|
OR cs.service_name ILIKE '%w/ backup%'
|
|
|
|
|
OR cs.description ILIKE '%workstation%backup%'
|
|
|
|
|
OR cs.description ILIKE '%w/ backup%'
|
|
|
|
|
OR cs.service_name ILIKE '%windows server%'
|
|
|
|
|
OR cs.service_name ILIKE '%server virtual%'
|
|
|
|
|
OR cs.service_name ILIKE '%server physical%'
|
|
|
|
|
OR cs.service_name ILIKE '%server phys%'
|
|
|
|
|
OR cs.service_name ILIKE '%esxi host%'
|
|
|
|
|
OR cs.service_name ILIKE '%wulf 365 it complete endpoint%'
|
|
|
|
|
OR cs.service_name ILIKE '%wulf 365 it complete server%'
|
|
|
|
|
OR cs.service_name ILIKE '%wulf it complete (server)%'
|
|
|
|
|
OR cs.service_name ILIKE '%wulf it complete (endpoint)%'
|
|
|
|
|
)
|
|
|
|
|
ORDER BY ct.company_id,
|
|
|
|
|
CASE
|
|
|
|
|
WHEN ct.contract_name ILIKE '%backup%' THEN 0
|
|
|
|
|
WHEN ct.contract_name ILIKE '%managed it%' THEN 1
|
|
|
|
|
WHEN ct.contract_name ILIKE '%fixed price%' THEN 2
|
|
|
|
|
ELSE 3
|
|
|
|
|
END
|
|
|
|
|
),
|
|
|
|
|
bi_backup AS (
|
|
|
|
|
SELECT DISTINCT ON (bi.company_id)
|
|
|
|
|
bi.company_id,
|
|
|
|
|
ct.id AS contract_id,
|
|
|
|
|
ct.contract_name,
|
|
|
|
|
bi.quantity AS contracted_qty
|
|
|
|
|
FROM billing_items bi
|
|
|
|
|
LEFT JOIN LATERAL (
|
|
|
|
|
SELECT id, contract_name
|
|
|
|
|
FROM contracts
|
|
|
|
|
WHERE company_id = bi.company_id
|
|
|
|
|
AND is_deleted = false AND status = 1
|
|
|
|
|
ORDER BY
|
|
|
|
|
CASE
|
|
|
|
|
WHEN contract_name ILIKE '%backup%' THEN 0
|
|
|
|
|
WHEN contract_name ILIKE '%managed it%' THEN 1
|
|
|
|
|
WHEN contract_name ILIKE '%fixed price%' THEN 2
|
|
|
|
|
ELSE 3
|
|
|
|
|
END
|
|
|
|
|
LIMIT 1
|
|
|
|
|
) ct ON true
|
|
|
|
|
WHERE bi.is_deleted = false
|
|
|
|
|
AND bi.description ILIKE '%windows workstation w/ backup%'
|
|
|
|
|
AND bi.synced_at::date = (
|
|
|
|
|
SELECT MAX(synced_at::date) FROM billing_items WHERE is_deleted = false
|
|
|
|
|
)
|
|
|
|
|
ORDER BY bi.company_id, bi.quantity DESC
|
|
|
|
|
),
|
|
|
|
|
coverage AS (
|
|
|
|
|
SELECT
|
|
|
|
|
company_id,
|
|
|
|
|
contract_id,
|
|
|
|
|
contract_name,
|
|
|
|
|
contracted_qty,
|
|
|
|
|
'contract_services' AS source
|
|
|
|
|
FROM cs_backup
|
|
|
|
|
UNION ALL
|
|
|
|
|
SELECT
|
|
|
|
|
b.company_id,
|
|
|
|
|
b.contract_id,
|
|
|
|
|
b.contract_name,
|
|
|
|
|
b.contracted_qty,
|
|
|
|
|
'billing_items' AS source
|
|
|
|
|
FROM bi_backup b
|
|
|
|
|
WHERE NOT EXISTS (SELECT 1 FROM cs_backup cs WHERE cs.company_id = b.company_id)
|
|
|
|
|
)
|
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
|
|
|
SELECT
|
|
|
|
|
cr.*,
|
2026-03-11 09:34:51 -04:00
|
|
|
c.company_name,
|
|
|
|
|
cov.contract_name AS billing_contract_name,
|
|
|
|
|
cov.contract_id AS billing_contract_id,
|
|
|
|
|
(cov.company_id IS NOT NULL) AS billing_covered,
|
|
|
|
|
cov.contracted_qty::int AS billing_contracted_qty,
|
|
|
|
|
cov.source AS coverage_source
|
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
|
|
|
FROM veeam_compliance_results cr
|
|
|
|
|
LEFT JOIN companies c ON c.id = cr.company_id
|
2026-03-11 09:34:51 -04:00
|
|
|
LEFT JOIN coverage cov ON cov.company_id = cr.company_id
|
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
|
|
|
ORDER BY cr.mismatch_type, c.company_name, cr.device_name
|
|
|
|
|
`);
|
|
|
|
|
|
|
|
|
|
return NextResponse.json({
|
|
|
|
|
summary: {
|
|
|
|
|
totalContractedDevices: totalContracted,
|
|
|
|
|
matchedDevices: Math.max(0, matched),
|
|
|
|
|
contractedNotBackedUp,
|
|
|
|
|
backedUpNotContracted,
|
|
|
|
|
computedAt: lastComputed.rows[0]?.computed_at || null,
|
|
|
|
|
},
|
|
|
|
|
mismatches: mismatches.rows,
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('[VEEAM-API] compliance error:', error);
|
|
|
|
|
return NextResponse.json({
|
|
|
|
|
summary: {
|
|
|
|
|
totalContractedDevices: 0,
|
|
|
|
|
matchedDevices: 0,
|
|
|
|
|
contractedNotBackedUp: 0,
|
|
|
|
|
backedUpNotContracted: 0,
|
|
|
|
|
computedAt: null,
|
|
|
|
|
},
|
|
|
|
|
mismatches: [],
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|