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
This commit is contained in:
lorentz 2026-03-11 09:34:51 -04:00
parent 19605f82aa
commit c518eefdb2
61 changed files with 11236 additions and 237 deletions

View file

@ -16,6 +16,7 @@ import {
buildActiveFilter,
buildDateRangeFilter,
buildContractsFilter,
buildContractServicesFilter,
buildProjectsFilter,
buildTimeEntriesFilter,
buildBillingItemsFilter,
@ -125,6 +126,9 @@ export class EntitySyncService {
if (entity === EntityType.CONTRACTS) {
filters.push(...buildContractsFilter());
entityLogger.info('Full sync with status filter for active contracts');
} else if (entity === EntityType.CONTRACT_SERVICES) {
filters.push(...buildContractServicesFilter());
entityLogger.info('Full sync of all contract services');
} else if (entity === EntityType.PROJECTS) {
filters.push(...buildProjectsFilter());
entityLogger.info('Full sync with status filter for non-completed projects');
@ -365,6 +369,21 @@ export class EntitySyncService {
entityLogger.info('Upserted records to PostgreSQL', { upsertedCount });
// Post-sync enrichment: populate service_name from autotask_services lookup
if (entity === EntityType.CONTRACT_SERVICES) {
try {
const enrichResult = await postgresClient.query(`
UPDATE contract_services cs
SET service_name = s.name
FROM autotask_services s
WHERE cs.service_id = s.id AND cs.service_name IS NULL AND s.name IS NOT NULL
`);
entityLogger.info('Enriched contract_services with service names', { rowCount: enrichResult.rowCount });
} catch (err) {
entityLogger.warn('service_name enrichment skipped (autotask_services may not be synced yet)');
}
}
// For full sync, soft delete records not in the fetched set
// IMPORTANT: Skip soft deletes if ANY filters were applied (date, status, active, etc.)
// because we cannot know what records exist outside the filter criteria