wulf-pulse/app/api/veeam/contract-coverage/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

181 lines
6.9 KiB
TypeScript

/**
* Contract Coverage API
* GET /api/veeam/contract-coverage
* Returns per-company contracted vs deployed counts for Servers, Workstations, M365
* plus the individual contract service lines for the expanded view.
*/
import { NextResponse } from 'next/server';
import postgresClient from '@/lib/services/postgres-client';
export async function GET() {
try {
const result = await postgresClient.query(`
WITH
-- All active contract service lines with category classification
cs_lines AS (
SELECT
ct.company_id,
ct.id AS contract_id,
ct.contract_name,
cs.id AS cs_id,
cs.service_name AS line_name,
cs.unit_price,
cs.unit_cost,
CASE
WHEN cs.service_name ILIKE '%windows server%'
OR cs.service_name ILIKE '%server virtual%'
OR cs.service_name ILIKE '%server phys%'
OR cs.service_name ILIKE '%esxi host%'
OR cs.service_name ILIKE '%wulf 365 it complete server%'
OR cs.service_name ILIKE '%wulf it complete (server)%'
THEN 'server'
WHEN cs.service_name ILIKE '%workstation%backup%'
OR cs.service_name ILIKE '%w/ backup%'
OR cs.service_name ILIKE '%wulf 365 it complete endpoint%'
OR cs.service_name ILIKE '%wulf it complete (endpoint)%'
THEN 'workstation'
WHEN cs.service_name ILIKE '%microsoft 365%'
OR cs.service_name ILIKE '%office 365%'
OR cs.service_name ILIKE '%exchange online%'
OR cs.service_name ILIKE '%m365%'
OR cs.service_name ILIKE '%veeam backup for microsoft office 365%'
THEN 'm365'
ELSE 'other'
END AS category
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
),
-- Summarise contracted counts per company
contracted AS (
SELECT
company_id,
COUNT(*) FILTER (WHERE category = 'server') AS contracted_servers,
COUNT(*) FILTER (WHERE category = 'workstation') AS contracted_workstations,
COUNT(*) FILTER (WHERE category = 'm365') AS contracted_m365,
COUNT(*) FILTER (WHERE category = 'other') AS contracted_other,
COUNT(*) AS contracted_total
FROM cs_lines
GROUP BY company_id
),
-- Deployed counts from Veeam agent jobs
deployed AS (
SELECT
o.company_id,
COUNT(*) FILTER (WHERE j.operation_mode = 'Server') AS deployed_servers,
COUNT(*) FILTER (WHERE j.operation_mode = 'Workstation') AS deployed_workstations,
COUNT(*) FILTER (WHERE j.operation_mode NOT IN ('Server','Workstation')) AS deployed_other
FROM veeam_backup_agent_jobs j
JOIN veeam_organizations o ON o.instance_uid = j.organization_uid
WHERE j.is_enabled = true
GROUP BY o.company_id
),
-- All companies that appear in either side
all_companies AS (
SELECT company_id FROM contracted
UNION
SELECT company_id FROM deployed
)
SELECT
ac.company_id,
c.company_name,
COALESCE(ct.contracted_servers, 0) AS contracted_servers,
COALESCE(ct.contracted_workstations, 0) AS contracted_workstations,
COALESCE(ct.contracted_m365, 0) AS contracted_m365,
COALESCE(ct.contracted_other, 0) AS contracted_other,
COALESCE(d.deployed_servers, 0) AS deployed_servers,
COALESCE(d.deployed_workstations, 0) AS deployed_workstations,
COALESCE(d.deployed_other, 0) AS deployed_other
FROM all_companies ac
JOIN companies c ON c.id = ac.company_id
LEFT JOIN contracted ct ON ct.company_id = ac.company_id
LEFT JOIN deployed d ON d.company_id = ac.company_id
ORDER BY c.company_name
`);
// Build per-company service lines map
const linesResult = await postgresClient.query(`
SELECT
ct.company_id,
ct.id AS contract_id,
ct.contract_name,
cs.id AS cs_id,
cs.service_name AS line_name,
cs.unit_price,
cs.unit_cost,
CASE
WHEN cs.service_name ILIKE '%windows server%'
OR cs.service_name ILIKE '%server virtual%'
OR cs.service_name ILIKE '%server phys%'
OR cs.service_name ILIKE '%esxi host%'
OR cs.service_name ILIKE '%wulf 365 it complete server%'
OR cs.service_name ILIKE '%wulf it complete (server)%'
THEN 'server'
WHEN cs.service_name ILIKE '%workstation%backup%'
OR cs.service_name ILIKE '%w/ backup%'
OR cs.service_name ILIKE '%wulf 365 it complete endpoint%'
OR cs.service_name ILIKE '%wulf it complete (endpoint)%'
THEN 'workstation'
WHEN cs.service_name ILIKE '%microsoft 365%'
OR cs.service_name ILIKE '%office 365%'
OR cs.service_name ILIKE '%exchange online%'
OR cs.service_name ILIKE '%m365%'
OR cs.service_name ILIKE '%veeam backup for microsoft office 365%'
THEN 'm365'
ELSE 'other'
END AS category
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
ORDER BY ct.company_id, ct.contract_name, cs.service_name
`);
// Group lines by company_id
const linesByCompany: Record<number, typeof linesResult.rows> = {};
for (const row of linesResult.rows) {
const cid = row.company_id;
if (!linesByCompany[cid]) linesByCompany[cid] = [];
linesByCompany[cid].push(row);
}
const rows = result.rows.map((r) => ({
company_id: r.company_id,
company_name: r.company_name,
contracted: {
servers: Number(r.contracted_servers),
workstations: Number(r.contracted_workstations),
m365: Number(r.contracted_m365),
other: Number(r.contracted_other),
},
deployed: {
servers: Number(r.deployed_servers),
workstations: Number(r.deployed_workstations),
other: Number(r.deployed_other),
},
lines: (linesByCompany[r.company_id] || []).map((l) => ({
cs_id: l.cs_id,
contract_id: l.contract_id,
contract_name: l.contract_name,
line_name: l.line_name,
unit_price: l.unit_price != null ? Number(l.unit_price) : null,
unit_cost: l.unit_cost != null ? Number(l.unit_cost) : null,
category: l.category,
})),
}));
return NextResponse.json({ rows });
} catch (error) {
console.error('[contract-coverage] error:', error);
return NextResponse.json({ rows: [] });
}
}