/** * GET /api/dashboard/overview * Single round-trip backing the new dashboard. All queries run in parallel. * * today — KPI snapshot: opened, resolved, open total, SLA breaches * attention — counts that should pull a human's eyes * observations — recent device_observations (loglift et al.) * audits — recent endpoint_audits * syncHealth — per-schedule last_run / last_status (consumed by /status) * stats — small footer: companies, CIs, xref linkage */ import { NextResponse } from 'next/server'; import { requireAuth } from '@/lib/auth-utils'; import postgresClient from '@/lib/services/postgres-client'; export async function GET() { const { error } = await requireAuth(); if (error) return error; type Counts = { count: string }; const [ todayRes, yesterdayOpenedRes, last7AvgResolvedRes, linkConflictsRes, itglueUnlinkedRes, s1UnmappedRes, schedulesRes, observationsRes, auditsRes, syncHealthRes, companiesRes, ciRes, xrefRes, ] = await Promise.all([ /* today snapshot — single row, all four KPIs */ postgresClient.query<{ opened_today: string; resolved_today: string; open_total: string; sla_breaches: string; }>(` SELECT COUNT(*) FILTER (WHERE create_date::date = CURRENT_DATE)::text AS opened_today, COUNT(*) FILTER (WHERE completed_date::date = CURRENT_DATE)::text AS resolved_today, COUNT(*) FILTER (WHERE completed_date IS NULL)::text AS open_total, COUNT(*) FILTER ( WHERE completed_date IS NULL AND due_date_time IS NOT NULL AND due_date_time < NOW() )::text AS sla_breaches FROM tickets WHERE is_deleted = false OR is_deleted IS NULL `), /* yesterday's opened count for the today-vs-yesterday delta */ postgresClient.query<{ count: string }>(` SELECT COUNT(*)::text AS count FROM tickets WHERE create_date::date = CURRENT_DATE - INTERVAL '1 day' AND (is_deleted = false OR is_deleted IS NULL) `), /* 7-day average resolved (excluding today) for the resolved delta */ postgresClient.query<{ avg_resolved: string }>(` SELECT COALESCE(AVG(daily_count), 0)::text AS avg_resolved FROM ( SELECT completed_date::date AS d, COUNT(*) AS daily_count FROM tickets WHERE completed_date >= CURRENT_DATE - INTERVAL '7 days' AND completed_date < CURRENT_DATE AND (is_deleted = false OR is_deleted IS NULL) GROUP BY completed_date::date ) sub `), postgresClient.query( `SELECT COUNT(*)::text AS count FROM device_link_review WHERE resolved_at IS NULL` ), postgresClient.query( `SELECT COUNT(*)::text AS count FROM device_external_ids WHERE source = 'itglue' AND configuration_item_id IS NULL` ), postgresClient.query( `SELECT COUNT(*)::text AS count FROM device_external_ids WHERE source = 's1' AND configuration_item_id IS NULL` ), postgresClient.query<{ enabled: string; total: string }>( `SELECT COUNT(*) FILTER (WHERE is_enabled)::text AS enabled, COUNT(*)::text AS total FROM sync_schedules` ), postgresClient.query<{ id: string; kind: string; source: string; collected_at: string; hostname: string | null; company_name: string | null; run_id: string | null; }>( `SELECT o.id::text, o.kind, o.source, o.collected_at::text, ci.reference_title AS hostname, c.company_name, o.run_id FROM device_observations o LEFT JOIN configuration_items ci ON ci.id = o.configuration_item_id LEFT JOIN companies c ON c.id = ci.company_id ORDER BY o.collected_at DESC LIMIT 10` ), postgresClient.query<{ id: string; generated_at: string; hostname: string | null; company_name: string | null; overall_score: string | null; field_gaps_count: string; status: string; }>( `SELECT a.id::text, a.generated_at::text, ci.reference_title AS hostname, c.company_name, a.overall_score::text, jsonb_array_length(COALESCE(a.field_gaps, '[]'::jsonb))::text AS field_gaps_count, a.status FROM endpoint_audits a LEFT JOIN configuration_items ci ON ci.id = a.configuration_item_id LEFT JOIN companies c ON c.id = ci.company_id ORDER BY a.generated_at DESC LIMIT 10` ), postgresClient.query<{ id: string; name: string; sync_type: string; is_enabled: boolean; last_run: string | null; last_status: string | null; last_error: string | null; next_run: string | null; }>( `SELECT id, name, sync_type, is_enabled, last_run::text, last_status, last_error, next_run::text FROM sync_schedules ORDER BY name` ), postgresClient.query( `SELECT COUNT(*)::text AS count FROM companies WHERE company_type = 1 AND is_active = true` ), postgresClient.query( `SELECT COUNT(*)::text AS count FROM configuration_items WHERE is_deleted = false OR is_deleted IS NULL` ), postgresClient.query<{ total: string; linked: string }>( `SELECT COUNT(*)::text AS total, COUNT(*) FILTER (WHERE configuration_item_id IS NOT NULL)::text AS linked FROM device_external_ids` ), ]); const today = todayRes.rows[0]; const yesterdayOpened = parseInt(yesterdayOpenedRes.rows[0]?.count ?? '0', 10); const last7Avg = parseFloat(last7AvgResolvedRes.rows[0]?.avg_resolved ?? '0'); return NextResponse.json({ today: { openedToday: parseInt(today?.opened_today ?? '0', 10), resolvedToday: parseInt(today?.resolved_today ?? '0', 10), openTotal: parseInt(today?.open_total ?? '0', 10), slaBreaches: parseInt(today?.sla_breaches ?? '0', 10), yesterdayOpened, last7DayAvgResolved: Math.round(last7Avg * 10) / 10, }, attention: { linkConflicts: parseInt(linkConflictsRes.rows[0]?.count ?? '0', 10), itglueUnlinked: parseInt(itglueUnlinkedRes.rows[0]?.count ?? '0', 10), s1Unmapped: parseInt(s1UnmappedRes.rows[0]?.count ?? '0', 10), schedules: { enabled: parseInt(schedulesRes.rows[0]?.enabled ?? '0', 10), total: parseInt(schedulesRes.rows[0]?.total ?? '0', 10), }, }, observations: observationsRes.rows.map((r) => ({ id: r.id, kind: r.kind, source: r.source, collectedAt: r.collected_at, hostname: r.hostname, companyName: r.company_name, runId: r.run_id, })), audits: auditsRes.rows.map((r) => ({ id: r.id, generatedAt: r.generated_at, hostname: r.hostname, companyName: r.company_name, overallScore: r.overall_score === null ? null : Number(r.overall_score), fieldGapsCount: parseInt(r.field_gaps_count, 10), status: r.status, })), syncHealth: syncHealthRes.rows.map((r) => ({ id: r.id, name: r.name, syncType: r.sync_type, isEnabled: r.is_enabled, lastRun: r.last_run, lastStatus: r.last_status, lastError: r.last_error, nextRun: r.next_run, })), stats: { activeCompanies: parseInt(companiesRes.rows[0]?.count ?? '0', 10), configurationItems: parseInt(ciRes.rows[0]?.count ?? '0', 10), xref: { total: parseInt(xrefRes.rows[0]?.total ?? '0', 10), linked: parseInt(xrefRes.rows[0]?.linked ?? '0', 10), }, }, }); }