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

@ -7,15 +7,21 @@ import cron, { ScheduledTask } from 'node-cron';
import { SyncService, createSyncService } from './sync-service';
import { postgresClient } from './postgres-client';
import { AutotaskClient } from './autotask-client';
import { EntityType, SyncType } from '../types/sync';
import { VeeamSyncService } from './veeam-sync-service';
import { VeeamRpoService } from './veeam-rpo-service';
import { EngagementSyncService } from './engagement-sync-service';
import { isMsgraphConfigured } from './msgraph-factory';
import { ZoomSyncService } from './zoom-sync-service';
import { isZoomConfigured } from './zoom-factory';
import { MorningSummaryService } from './morning-summary-service';
export interface ScheduleConfig {
id: string;
name: string;
description: string;
cron_expression: string;
sync_type: 'incremental' | 'full' | 'veeam-incremental' | 'veeam-full' | 'veeam-rpo-check';
sync_type: 'incremental' | 'full' | 'veeam-incremental' | 'veeam-full' | 'veeam-rpo-check' | 'contract-services' | 'engagement-daily' | 'zoom-daily' | 'morning-summary';
years_back?: number;
is_enabled: boolean;
last_run?: Date;
@ -40,6 +46,8 @@ class SyncScheduler {
private syncService: SyncService;
private _veeamSyncService: VeeamSyncService | null = null;
private _veeamRpoService: VeeamRpoService | null = null;
private _engagementSyncService: EngagementSyncService | null = null;
private _zoomSyncService: ZoomSyncService | null = null;
private getVeeamSyncService(): VeeamSyncService {
if (!this._veeamSyncService) {
@ -55,6 +63,28 @@ class SyncScheduler {
return this._veeamRpoService;
}
private getEngagementSyncService(): EngagementSyncService {
if (!this._engagementSyncService) {
this._engagementSyncService = new EngagementSyncService();
}
return this._engagementSyncService;
}
private _morningSummaryService?: MorningSummaryService;
private getMorningSummaryService(): MorningSummaryService {
if (!this._morningSummaryService) {
this._morningSummaryService = new MorningSummaryService();
}
return this._morningSummaryService;
}
private getZoomSyncService(): ZoomSyncService {
if (!this._zoomSyncService) {
this._zoomSyncService = new ZoomSyncService();
}
return this._zoomSyncService;
}
constructor() {
// Create sync service instance
const autotaskClient = new AutotaskClient({
@ -105,7 +135,7 @@ class SyncScheduler {
name VARCHAR(100) NOT NULL,
description TEXT,
cron_expression VARCHAR(50) NOT NULL,
sync_type VARCHAR(30) NOT NULL CHECK (sync_type IN ('incremental', 'full', 'veeam-incremental', 'veeam-full')),
sync_type VARCHAR(30) NOT NULL,
years_back INTEGER DEFAULT 2,
is_enabled BOOLEAN NOT NULL DEFAULT true,
last_run TIMESTAMP,
@ -180,6 +210,38 @@ class SyncScheduler {
sync_type: 'veeam-rpo-check',
is_enabled: false,
},
{
id: 'contract-services',
name: 'Contract Services Sync',
description: 'Syncs Autotask contract service lines (service catalog items per contract) daily at 4 AM',
cron_expression: '0 4 * * *',
sync_type: 'contract-services',
is_enabled: true,
},
{
id: 'engagement-daily',
name: 'Engagement Daily Sync',
description: 'Syncs Microsoft Graph Teams and email activity for employee engagement dashboard daily at 6 AM',
cron_expression: '0 6 * * *',
sync_type: 'engagement-daily',
is_enabled: false,
},
{
id: 'zoom-daily',
name: 'Zoom Daily Sync',
description: 'Syncs Zoom Phone call logs and meeting data daily at 6 AM',
cron_expression: '0 6 * * *',
sync_type: 'zoom-daily',
is_enabled: false,
},
{
id: 'morning-summary',
name: 'Morning NOC Summary',
description: 'Posts a Zabbix overnight summary Adaptive Card to configured Teams channel webhooks at 6:30 AM MonFri',
cron_expression: '30 6 * * 1-5',
sync_type: 'morning-summary',
is_enabled: false,
},
];
for (const schedule of defaultSchedules) {
@ -289,6 +351,22 @@ class SyncScheduler {
await this.getVeeamSyncService().fullSync('scheduled');
} else if (config.sync_type === 'veeam-rpo-check') {
await this.getVeeamRpoService().runCheck();
} else if (config.sync_type === 'contract-services') {
await this.syncService.syncEntities([EntityType.AUTOTASK_SERVICES, EntityType.CONTRACT_SERVICES], SyncType.ENTITY_SPECIFIC, 'scheduled');
} else if (config.sync_type === 'engagement-daily') {
if (isMsgraphConfigured()) {
await this.getEngagementSyncService().sync();
} else {
console.log('[SCHEDULER] Skipping engagement sync — Microsoft Graph not configured');
}
} else if (config.sync_type === 'zoom-daily') {
if (isZoomConfigured()) {
await this.getZoomSyncService().sync();
} else {
console.log('[SCHEDULER] Skipping Zoom sync — Zoom credentials not configured');
}
} else if (config.sync_type === 'morning-summary') {
await this.getMorningSummaryService().run();
} else if (config.sync_type === 'incremental') {
await this.syncService.incrementalSync('scheduled');
} else {