wulf-pulse/app/api/veeam/compliance/route.ts
lorentz c518eefdb2 feat: Morning NOC Summary adaptive card for Teams
- Add MorningSummaryService with Zabbix aggregation and adaptive card builder
- Add webhook delivery system with Teams incoming webhooks
- Add admin UI at /admin/morning-summary for webhook/config management
- Add API routes: /send, /test, /webhooks, /webhooks/[id], /config, /history
- Register morning-summary cron job in SyncScheduler (Mon-Fri 6:30 AM)
- Add outages_only filter (Unavailable triggers only)
- Fix host resolution: use getTriggerEnabledHosts to exclude disabled hosts
- Fix resolved events: event.get value:1 scoped to window with r_eventid filter
- Remove emojis from fact rows and section headers in card
- Remove Open Zabbix button (duplicate of View Problems)
- Add migrations: morning_summary_config + morning_summaries tables
- Add outages_only column to morning_summary_config
2026-03-11 09:34:51 -04:00

165 lines
5.9 KiB
TypeScript

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'
);
// 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.
const mismatches = await postgresClient.query(`
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)
)
SELECT
cr.*,
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
FROM veeam_compliance_results cr
LEFT JOIN companies c ON c.id = cr.company_id
LEFT JOIN coverage cov ON cov.company_id = cr.company_id
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: [],
});
}
}