- 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
165 lines
5.9 KiB
TypeScript
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: [],
|
|
});
|
|
}
|
|
}
|