2026-01-26 10:24:58 -05:00
|
|
|
|
/**
|
|
|
|
|
|
* Sync Scheduler Service
|
|
|
|
|
|
* Manages scheduled automatic syncs using node-cron
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import cron, { ScheduledTask } from 'node-cron';
|
2026-01-26 10:29:31 -05:00
|
|
|
|
import { SyncService, createSyncService } from './sync-service';
|
2026-01-26 10:24:58 -05:00
|
|
|
|
import { postgresClient } from './postgres-client';
|
2026-01-26 10:29:31 -05:00
|
|
|
|
import { AutotaskClient } from './autotask-client';
|
2026-03-11 09:34:51 -04:00
|
|
|
|
import { EntityType, SyncType } from '../types/sync';
|
feat: Veeam VSPC backup integration - sync, compliance, UI
- Database: 7 Veeam tables + backup_type_udf column on configuration_items
- API Client: VSPC REST API v3 client with pagination, rate limiting, Bearer auth
- Sync Service: full/incremental sync for orgs, servers, repos, jobs, agent jobs, workloads
- Scheduler: veeam-incremental (30min) and veeam-full (daily 2AM) schedules
- Compliance Engine: cross-references Autotask config items vs Veeam workloads
- API Endpoints: backup-status, companies, workloads, jobs, repos, compliance, sync
- UI: Backup Status page with Overview + Contract Compliance tabs
- Navigation: added Backup Status link with HardDrive icon
- Docker: added VEEAM_VSPC_URL and VEEAM_VSPC_API_KEY env vars to compose
2026-02-11 21:04:28 -05:00
|
|
|
|
import { VeeamSyncService } from './veeam-sync-service';
|
2026-02-27 14:52:14 -05:00
|
|
|
|
import { VeeamRpoService } from './veeam-rpo-service';
|
2026-03-11 09:34:51 -04:00
|
|
|
|
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';
|
feat: QuickBooks Online integration
- Add QBO OAuth2 client with token refresh (lib/services/qbo-client.ts)
- Add QBO sync service for invoices, payments, deposits, purchases, journal entries, reports (lib/services/qbo-sync-service.ts)
- Add QBO types (lib/types/qbo.ts)
- Add API routes: /api/qbo/auth, /api/qbo/sync, /api/qbo/disconnect
- Add /admin/qbo status and sync management page
- Add legal pages: /legal/eula, /legal/privacy (Intuit app assessment)
- Add QBO nav link under Admin
- Fix reports: remove invalid summarize_column_by, add accounting_method from Preferences API, add showrows=all&showcols=all
- Add CashFlow report type alongside P&L and BalanceSheet
- Add NoReportData check to skip empty report months
- Add intuit_tid capture in error messages
- Add redirect: follow for cluster routing
- Migration 051: qbo_tokens, qbo_invoices, qbo_payments, qbo_deposits, qbo_transactions, qbo_reports tables
Also includes earlier work:
- Ping flap suppression pipeline step
- Ticket digest reports with LLM analysis
- Zabbix WAN monitor and gap analysis
- Kiosk is_deleted filter fixes
- Datto RMM ping target enrichment
- Entity sync soft-delete detection
2026-03-17 07:39:55 -04:00
|
|
|
|
import { TicketDigestService } from './ticket-digest-service';
|
2026-01-26 10:24:58 -05:00
|
|
|
|
|
|
|
|
|
|
export interface ScheduleConfig {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
description: string;
|
|
|
|
|
|
cron_expression: string;
|
feat: QuickBooks Online integration
- Add QBO OAuth2 client with token refresh (lib/services/qbo-client.ts)
- Add QBO sync service for invoices, payments, deposits, purchases, journal entries, reports (lib/services/qbo-sync-service.ts)
- Add QBO types (lib/types/qbo.ts)
- Add API routes: /api/qbo/auth, /api/qbo/sync, /api/qbo/disconnect
- Add /admin/qbo status and sync management page
- Add legal pages: /legal/eula, /legal/privacy (Intuit app assessment)
- Add QBO nav link under Admin
- Fix reports: remove invalid summarize_column_by, add accounting_method from Preferences API, add showrows=all&showcols=all
- Add CashFlow report type alongside P&L and BalanceSheet
- Add NoReportData check to skip empty report months
- Add intuit_tid capture in error messages
- Add redirect: follow for cluster routing
- Migration 051: qbo_tokens, qbo_invoices, qbo_payments, qbo_deposits, qbo_transactions, qbo_reports tables
Also includes earlier work:
- Ping flap suppression pipeline step
- Ticket digest reports with LLM analysis
- Zabbix WAN monitor and gap analysis
- Kiosk is_deleted filter fixes
- Datto RMM ping target enrichment
- Entity sync soft-delete detection
2026-03-17 07:39:55 -04:00
|
|
|
|
sync_type: 'incremental' | 'full' | 'veeam-incremental' | 'veeam-full' | 'veeam-rpo-check' | 'contract-services' | 'engagement-daily' | 'zoom-daily' | 'morning-summary' | 'ticket-digest-daily' | 'ticket-digest-weekly' | 'ticket-digest-monthly';
|
2026-01-26 10:24:58 -05:00
|
|
|
|
years_back?: number;
|
|
|
|
|
|
is_enabled: boolean;
|
|
|
|
|
|
last_run?: Date;
|
|
|
|
|
|
next_run?: Date;
|
|
|
|
|
|
last_status?: 'success' | 'failed';
|
|
|
|
|
|
last_error?: string;
|
|
|
|
|
|
created_at: Date;
|
|
|
|
|
|
updated_at: Date;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface ScheduleStatus {
|
|
|
|
|
|
config: ScheduleConfig;
|
|
|
|
|
|
isRunning: boolean;
|
|
|
|
|
|
isValid: boolean;
|
|
|
|
|
|
nextRun?: Date;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class SyncScheduler {
|
|
|
|
|
|
private tasks: Map<string, ScheduledTask> = new Map();
|
|
|
|
|
|
private runningJobs: Set<string> = new Set();
|
|
|
|
|
|
private initialized = false;
|
2026-01-26 10:29:31 -05:00
|
|
|
|
private syncService: SyncService;
|
feat: Veeam VSPC backup integration - sync, compliance, UI
- Database: 7 Veeam tables + backup_type_udf column on configuration_items
- API Client: VSPC REST API v3 client with pagination, rate limiting, Bearer auth
- Sync Service: full/incremental sync for orgs, servers, repos, jobs, agent jobs, workloads
- Scheduler: veeam-incremental (30min) and veeam-full (daily 2AM) schedules
- Compliance Engine: cross-references Autotask config items vs Veeam workloads
- API Endpoints: backup-status, companies, workloads, jobs, repos, compliance, sync
- UI: Backup Status page with Overview + Contract Compliance tabs
- Navigation: added Backup Status link with HardDrive icon
- Docker: added VEEAM_VSPC_URL and VEEAM_VSPC_API_KEY env vars to compose
2026-02-11 21:04:28 -05:00
|
|
|
|
private _veeamSyncService: VeeamSyncService | null = null;
|
2026-02-27 14:52:14 -05:00
|
|
|
|
private _veeamRpoService: VeeamRpoService | null = null;
|
2026-03-11 09:34:51 -04:00
|
|
|
|
private _engagementSyncService: EngagementSyncService | null = null;
|
|
|
|
|
|
private _zoomSyncService: ZoomSyncService | null = null;
|
feat: Veeam VSPC backup integration - sync, compliance, UI
- Database: 7 Veeam tables + backup_type_udf column on configuration_items
- API Client: VSPC REST API v3 client with pagination, rate limiting, Bearer auth
- Sync Service: full/incremental sync for orgs, servers, repos, jobs, agent jobs, workloads
- Scheduler: veeam-incremental (30min) and veeam-full (daily 2AM) schedules
- Compliance Engine: cross-references Autotask config items vs Veeam workloads
- API Endpoints: backup-status, companies, workloads, jobs, repos, compliance, sync
- UI: Backup Status page with Overview + Contract Compliance tabs
- Navigation: added Backup Status link with HardDrive icon
- Docker: added VEEAM_VSPC_URL and VEEAM_VSPC_API_KEY env vars to compose
2026-02-11 21:04:28 -05:00
|
|
|
|
|
|
|
|
|
|
private getVeeamSyncService(): VeeamSyncService {
|
|
|
|
|
|
if (!this._veeamSyncService) {
|
|
|
|
|
|
this._veeamSyncService = new VeeamSyncService();
|
|
|
|
|
|
}
|
|
|
|
|
|
return this._veeamSyncService;
|
|
|
|
|
|
}
|
2026-01-26 10:29:31 -05:00
|
|
|
|
|
2026-02-27 14:52:14 -05:00
|
|
|
|
private getVeeamRpoService(): VeeamRpoService {
|
|
|
|
|
|
if (!this._veeamRpoService) {
|
|
|
|
|
|
this._veeamRpoService = new VeeamRpoService();
|
|
|
|
|
|
}
|
|
|
|
|
|
return this._veeamRpoService;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 09:34:51 -04:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat: QuickBooks Online integration
- Add QBO OAuth2 client with token refresh (lib/services/qbo-client.ts)
- Add QBO sync service for invoices, payments, deposits, purchases, journal entries, reports (lib/services/qbo-sync-service.ts)
- Add QBO types (lib/types/qbo.ts)
- Add API routes: /api/qbo/auth, /api/qbo/sync, /api/qbo/disconnect
- Add /admin/qbo status and sync management page
- Add legal pages: /legal/eula, /legal/privacy (Intuit app assessment)
- Add QBO nav link under Admin
- Fix reports: remove invalid summarize_column_by, add accounting_method from Preferences API, add showrows=all&showcols=all
- Add CashFlow report type alongside P&L and BalanceSheet
- Add NoReportData check to skip empty report months
- Add intuit_tid capture in error messages
- Add redirect: follow for cluster routing
- Migration 051: qbo_tokens, qbo_invoices, qbo_payments, qbo_deposits, qbo_transactions, qbo_reports tables
Also includes earlier work:
- Ping flap suppression pipeline step
- Ticket digest reports with LLM analysis
- Zabbix WAN monitor and gap analysis
- Kiosk is_deleted filter fixes
- Datto RMM ping target enrichment
- Entity sync soft-delete detection
2026-03-17 07:39:55 -04:00
|
|
|
|
private _ticketDigestService?: TicketDigestService;
|
|
|
|
|
|
private getTicketDigestService(): TicketDigestService {
|
|
|
|
|
|
if (!this._ticketDigestService) {
|
|
|
|
|
|
this._ticketDigestService = new TicketDigestService();
|
|
|
|
|
|
}
|
|
|
|
|
|
return this._ticketDigestService;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 09:34:51 -04:00
|
|
|
|
private getZoomSyncService(): ZoomSyncService {
|
|
|
|
|
|
if (!this._zoomSyncService) {
|
|
|
|
|
|
this._zoomSyncService = new ZoomSyncService();
|
|
|
|
|
|
}
|
|
|
|
|
|
return this._zoomSyncService;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 10:29:31 -05:00
|
|
|
|
constructor() {
|
|
|
|
|
|
// Create sync service instance
|
|
|
|
|
|
const autotaskClient = new AutotaskClient({
|
|
|
|
|
|
apiUrl: process.env.AUTOTASK_API_URL || '',
|
|
|
|
|
|
username: process.env.AUTOTASK_USERNAME || '',
|
|
|
|
|
|
password: process.env.AUTOTASK_SECRET || '',
|
2026-02-02 21:13:35 -05:00
|
|
|
|
apiIntegrationCode: process.env.AUTOTASK_API_INTEGRATION_CODE || '',
|
2026-01-26 10:29:31 -05:00
|
|
|
|
});
|
|
|
|
|
|
this.syncService = createSyncService(autotaskClient);
|
|
|
|
|
|
}
|
2026-01-26 10:24:58 -05:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Initialize the scheduler and load schedules from database
|
|
|
|
|
|
*/
|
|
|
|
|
|
async initialize(): Promise<void> {
|
|
|
|
|
|
if (this.initialized) {
|
|
|
|
|
|
console.log('[SCHEDULER] Already initialized');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log('[SCHEDULER] Initializing sync scheduler...');
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// Create schedules table if it doesn't exist
|
|
|
|
|
|
await this.createSchedulesTable();
|
|
|
|
|
|
|
|
|
|
|
|
// Create default schedules if none exist
|
|
|
|
|
|
await this.createDefaultSchedules();
|
|
|
|
|
|
|
|
|
|
|
|
// Load and start all enabled schedules
|
|
|
|
|
|
await this.loadSchedules();
|
|
|
|
|
|
|
|
|
|
|
|
this.initialized = true;
|
|
|
|
|
|
console.log('[SCHEDULER] Sync scheduler initialized successfully');
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('[SCHEDULER] Failed to initialize:', error);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Create schedules table
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async createSchedulesTable(): Promise<void> {
|
|
|
|
|
|
const query = `
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS sync_schedules (
|
|
|
|
|
|
id VARCHAR(50) PRIMARY KEY,
|
|
|
|
|
|
name VARCHAR(100) NOT NULL,
|
|
|
|
|
|
description TEXT,
|
|
|
|
|
|
cron_expression VARCHAR(50) NOT NULL,
|
2026-03-11 09:34:51 -04:00
|
|
|
|
sync_type VARCHAR(30) NOT NULL,
|
2026-01-26 10:24:58 -05:00
|
|
|
|
years_back INTEGER DEFAULT 2,
|
|
|
|
|
|
is_enabled BOOLEAN NOT NULL DEFAULT true,
|
|
|
|
|
|
last_run TIMESTAMP,
|
|
|
|
|
|
next_run TIMESTAMP,
|
|
|
|
|
|
last_status VARCHAR(20),
|
|
|
|
|
|
last_error TEXT,
|
|
|
|
|
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
|
|
|
|
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_sync_schedules_enabled ON sync_schedules(is_enabled);
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_sync_schedules_next_run ON sync_schedules(next_run);
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
await postgresClient.query(query);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Create default schedules if none exist
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async createDefaultSchedules(): Promise<void> {
|
|
|
|
|
|
const countResult = await postgresClient.query(
|
|
|
|
|
|
'SELECT COUNT(*) as count FROM sync_schedules'
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (parseInt(countResult.rows[0].count) > 0) {
|
|
|
|
|
|
console.log('[SCHEDULER] Schedules already exist, skipping defaults');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log('[SCHEDULER] Creating default schedules...');
|
|
|
|
|
|
|
|
|
|
|
|
const defaultSchedules = [
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'daily-incremental',
|
|
|
|
|
|
name: 'Daily Incremental Sync',
|
|
|
|
|
|
description: 'Syncs changes from the last 24 hours every day at 2 AM',
|
|
|
|
|
|
cron_expression: '0 2 * * *',
|
|
|
|
|
|
sync_type: 'incremental',
|
feat: Veeam VSPC backup integration - sync, compliance, UI
- Database: 7 Veeam tables + backup_type_udf column on configuration_items
- API Client: VSPC REST API v3 client with pagination, rate limiting, Bearer auth
- Sync Service: full/incremental sync for orgs, servers, repos, jobs, agent jobs, workloads
- Scheduler: veeam-incremental (30min) and veeam-full (daily 2AM) schedules
- Compliance Engine: cross-references Autotask config items vs Veeam workloads
- API Endpoints: backup-status, companies, workloads, jobs, repos, compliance, sync
- UI: Backup Status page with Overview + Contract Compliance tabs
- Navigation: added Backup Status link with HardDrive icon
- Docker: added VEEAM_VSPC_URL and VEEAM_VSPC_API_KEY env vars to compose
2026-02-11 21:04:28 -05:00
|
|
|
|
is_enabled: false,
|
2026-01-26 10:24:58 -05:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'weekly-full',
|
|
|
|
|
|
name: 'Weekly Full Sync',
|
|
|
|
|
|
description: 'Full sync of all data every Sunday at 3 AM',
|
|
|
|
|
|
cron_expression: '0 3 * * 0',
|
|
|
|
|
|
sync_type: 'full',
|
|
|
|
|
|
years_back: 2,
|
feat: Veeam VSPC backup integration - sync, compliance, UI
- Database: 7 Veeam tables + backup_type_udf column on configuration_items
- API Client: VSPC REST API v3 client with pagination, rate limiting, Bearer auth
- Sync Service: full/incremental sync for orgs, servers, repos, jobs, agent jobs, workloads
- Scheduler: veeam-incremental (30min) and veeam-full (daily 2AM) schedules
- Compliance Engine: cross-references Autotask config items vs Veeam workloads
- API Endpoints: backup-status, companies, workloads, jobs, repos, compliance, sync
- UI: Backup Status page with Overview + Contract Compliance tabs
- Navigation: added Backup Status link with HardDrive icon
- Docker: added VEEAM_VSPC_URL and VEEAM_VSPC_API_KEY env vars to compose
2026-02-11 21:04:28 -05:00
|
|
|
|
is_enabled: false,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'veeam-incremental',
|
|
|
|
|
|
name: 'Veeam Incremental Sync',
|
|
|
|
|
|
description: 'Syncs Veeam backup data every 30 minutes',
|
|
|
|
|
|
cron_expression: '*/30 * * * *',
|
|
|
|
|
|
sync_type: 'veeam-incremental',
|
|
|
|
|
|
is_enabled: false,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'veeam-full',
|
|
|
|
|
|
name: 'Veeam Full Sync',
|
|
|
|
|
|
description: 'Full Veeam backup data sync daily at 2:00 AM',
|
|
|
|
|
|
cron_expression: '0 2 * * *',
|
|
|
|
|
|
sync_type: 'veeam-full',
|
|
|
|
|
|
is_enabled: false,
|
2026-01-26 10:24:58 -05:00
|
|
|
|
},
|
2026-02-27 14:52:14 -05:00
|
|
|
|
{
|
|
|
|
|
|
id: 'veeam-rpo-check',
|
|
|
|
|
|
name: 'Veeam RPO Check',
|
|
|
|
|
|
description: 'RPO-based workstation backup alerting — creates/resolves Autotask tickets every 30 minutes',
|
|
|
|
|
|
cron_expression: '*/30 * * * *',
|
|
|
|
|
|
sync_type: 'veeam-rpo-check',
|
|
|
|
|
|
is_enabled: false,
|
|
|
|
|
|
},
|
2026-03-11 09:34:51 -04:00
|
|
|
|
{
|
|
|
|
|
|
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 Mon–Fri',
|
|
|
|
|
|
cron_expression: '30 6 * * 1-5',
|
|
|
|
|
|
sync_type: 'morning-summary',
|
|
|
|
|
|
is_enabled: false,
|
|
|
|
|
|
},
|
feat: QuickBooks Online integration
- Add QBO OAuth2 client with token refresh (lib/services/qbo-client.ts)
- Add QBO sync service for invoices, payments, deposits, purchases, journal entries, reports (lib/services/qbo-sync-service.ts)
- Add QBO types (lib/types/qbo.ts)
- Add API routes: /api/qbo/auth, /api/qbo/sync, /api/qbo/disconnect
- Add /admin/qbo status and sync management page
- Add legal pages: /legal/eula, /legal/privacy (Intuit app assessment)
- Add QBO nav link under Admin
- Fix reports: remove invalid summarize_column_by, add accounting_method from Preferences API, add showrows=all&showcols=all
- Add CashFlow report type alongside P&L and BalanceSheet
- Add NoReportData check to skip empty report months
- Add intuit_tid capture in error messages
- Add redirect: follow for cluster routing
- Migration 051: qbo_tokens, qbo_invoices, qbo_payments, qbo_deposits, qbo_transactions, qbo_reports tables
Also includes earlier work:
- Ping flap suppression pipeline step
- Ticket digest reports with LLM analysis
- Zabbix WAN monitor and gap analysis
- Kiosk is_deleted filter fixes
- Datto RMM ping target enrichment
- Entity sync soft-delete detection
2026-03-17 07:39:55 -04:00
|
|
|
|
{
|
|
|
|
|
|
id: 'ticket-digest-daily',
|
|
|
|
|
|
name: 'Daily Ticket Digest',
|
|
|
|
|
|
description: 'LLM-analyzed ticket digest for the previous day, delivered to Teams at 7 AM Mon–Fri',
|
|
|
|
|
|
cron_expression: '0 7 * * 1-5',
|
|
|
|
|
|
sync_type: 'ticket-digest-daily',
|
|
|
|
|
|
is_enabled: false,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'ticket-digest-weekly',
|
|
|
|
|
|
name: 'Weekly Ticket Digest',
|
|
|
|
|
|
description: 'LLM-analyzed ticket digest for the previous week, delivered to Teams at 7 AM Monday',
|
|
|
|
|
|
cron_expression: '0 7 * * 1',
|
|
|
|
|
|
sync_type: 'ticket-digest-weekly',
|
|
|
|
|
|
is_enabled: false,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'ticket-digest-monthly',
|
|
|
|
|
|
name: 'Monthly Ticket Digest',
|
|
|
|
|
|
description: 'LLM-analyzed ticket digest for the previous month, delivered to Teams at 7 AM on the 1st',
|
|
|
|
|
|
cron_expression: '0 7 1 * *',
|
|
|
|
|
|
sync_type: 'ticket-digest-monthly',
|
|
|
|
|
|
is_enabled: false,
|
|
|
|
|
|
},
|
2026-01-26 10:24:58 -05:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
for (const schedule of defaultSchedules) {
|
|
|
|
|
|
await postgresClient.query(
|
|
|
|
|
|
`INSERT INTO sync_schedules (id, name, description, cron_expression, sync_type, years_back, is_enabled)
|
feat: Veeam VSPC backup integration - sync, compliance, UI
- Database: 7 Veeam tables + backup_type_udf column on configuration_items
- API Client: VSPC REST API v3 client with pagination, rate limiting, Bearer auth
- Sync Service: full/incremental sync for orgs, servers, repos, jobs, agent jobs, workloads
- Scheduler: veeam-incremental (30min) and veeam-full (daily 2AM) schedules
- Compliance Engine: cross-references Autotask config items vs Veeam workloads
- API Endpoints: backup-status, companies, workloads, jobs, repos, compliance, sync
- UI: Backup Status page with Overview + Contract Compliance tabs
- Navigation: added Backup Status link with HardDrive icon
- Docker: added VEEAM_VSPC_URL and VEEAM_VSPC_API_KEY env vars to compose
2026-02-11 21:04:28 -05:00
|
|
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
|
|
|
|
ON CONFLICT (id) DO NOTHING`,
|
2026-01-26 10:24:58 -05:00
|
|
|
|
[
|
|
|
|
|
|
schedule.id,
|
|
|
|
|
|
schedule.name,
|
|
|
|
|
|
schedule.description,
|
|
|
|
|
|
schedule.cron_expression,
|
|
|
|
|
|
schedule.sync_type,
|
|
|
|
|
|
schedule.years_back || null,
|
|
|
|
|
|
schedule.is_enabled,
|
|
|
|
|
|
]
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log('[SCHEDULER] Default schedules created');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Load all schedules from database and start enabled ones
|
|
|
|
|
|
*/
|
|
|
|
|
|
async loadSchedules(): Promise<void> {
|
|
|
|
|
|
console.log('[SCHEDULER] Loading schedules from database...');
|
|
|
|
|
|
|
|
|
|
|
|
const result = await postgresClient.query(
|
|
|
|
|
|
'SELECT * FROM sync_schedules ORDER BY id'
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const schedules = result.rows as ScheduleConfig[];
|
|
|
|
|
|
|
|
|
|
|
|
for (const schedule of schedules) {
|
|
|
|
|
|
if (schedule.is_enabled) {
|
|
|
|
|
|
this.startSchedule(schedule);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`[SCHEDULER] Loaded ${schedules.length} schedules (${this.tasks.size} active)`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Start a schedule
|
|
|
|
|
|
*/
|
|
|
|
|
|
private startSchedule(config: ScheduleConfig): void {
|
|
|
|
|
|
// Stop existing task if any
|
|
|
|
|
|
this.stopSchedule(config.id);
|
|
|
|
|
|
|
|
|
|
|
|
// Validate cron expression
|
|
|
|
|
|
if (!cron.validate(config.cron_expression)) {
|
|
|
|
|
|
console.error(`[SCHEDULER] Invalid cron expression for ${config.id}: ${config.cron_expression}`);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`[SCHEDULER] Starting schedule: ${config.name} (${config.cron_expression})`);
|
|
|
|
|
|
|
|
|
|
|
|
const task = cron.schedule(config.cron_expression, async () => {
|
|
|
|
|
|
await this.executeScheduledSync(config);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
this.tasks.set(config.id, task);
|
|
|
|
|
|
|
|
|
|
|
|
// Calculate and update next run time
|
|
|
|
|
|
this.updateNextRunTime(config.id, config.cron_expression);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Stop a schedule
|
|
|
|
|
|
*/
|
|
|
|
|
|
private stopSchedule(scheduleId: string): void {
|
|
|
|
|
|
const task = this.tasks.get(scheduleId);
|
|
|
|
|
|
if (task) {
|
|
|
|
|
|
task.stop();
|
|
|
|
|
|
this.tasks.delete(scheduleId);
|
|
|
|
|
|
console.log(`[SCHEDULER] Stopped schedule: ${scheduleId}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Execute a scheduled sync
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async executeScheduledSync(config: ScheduleConfig): Promise<void> {
|
|
|
|
|
|
// Prevent concurrent runs of the same schedule
|
|
|
|
|
|
if (this.runningJobs.has(config.id)) {
|
|
|
|
|
|
console.log(`[SCHEDULER] Schedule ${config.id} is already running, skipping`);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.runningJobs.add(config.id);
|
|
|
|
|
|
const startTime = new Date();
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`[SCHEDULER] Executing scheduled sync: ${config.name}`);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// Update last_run timestamp
|
|
|
|
|
|
await postgresClient.query(
|
|
|
|
|
|
'UPDATE sync_schedules SET last_run = NOW() WHERE id = $1',
|
|
|
|
|
|
[config.id]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Execute the sync
|
feat: Veeam VSPC backup integration - sync, compliance, UI
- Database: 7 Veeam tables + backup_type_udf column on configuration_items
- API Client: VSPC REST API v3 client with pagination, rate limiting, Bearer auth
- Sync Service: full/incremental sync for orgs, servers, repos, jobs, agent jobs, workloads
- Scheduler: veeam-incremental (30min) and veeam-full (daily 2AM) schedules
- Compliance Engine: cross-references Autotask config items vs Veeam workloads
- API Endpoints: backup-status, companies, workloads, jobs, repos, compliance, sync
- UI: Backup Status page with Overview + Contract Compliance tabs
- Navigation: added Backup Status link with HardDrive icon
- Docker: added VEEAM_VSPC_URL and VEEAM_VSPC_API_KEY env vars to compose
2026-02-11 21:04:28 -05:00
|
|
|
|
if (config.sync_type === 'veeam-incremental') {
|
|
|
|
|
|
await this.getVeeamSyncService().incrementalSync('scheduled');
|
|
|
|
|
|
} else if (config.sync_type === 'veeam-full') {
|
|
|
|
|
|
await this.getVeeamSyncService().fullSync('scheduled');
|
2026-02-27 14:52:14 -05:00
|
|
|
|
} else if (config.sync_type === 'veeam-rpo-check') {
|
|
|
|
|
|
await this.getVeeamRpoService().runCheck();
|
2026-03-11 09:34:51 -04:00
|
|
|
|
} 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();
|
feat: QuickBooks Online integration
- Add QBO OAuth2 client with token refresh (lib/services/qbo-client.ts)
- Add QBO sync service for invoices, payments, deposits, purchases, journal entries, reports (lib/services/qbo-sync-service.ts)
- Add QBO types (lib/types/qbo.ts)
- Add API routes: /api/qbo/auth, /api/qbo/sync, /api/qbo/disconnect
- Add /admin/qbo status and sync management page
- Add legal pages: /legal/eula, /legal/privacy (Intuit app assessment)
- Add QBO nav link under Admin
- Fix reports: remove invalid summarize_column_by, add accounting_method from Preferences API, add showrows=all&showcols=all
- Add CashFlow report type alongside P&L and BalanceSheet
- Add NoReportData check to skip empty report months
- Add intuit_tid capture in error messages
- Add redirect: follow for cluster routing
- Migration 051: qbo_tokens, qbo_invoices, qbo_payments, qbo_deposits, qbo_transactions, qbo_reports tables
Also includes earlier work:
- Ping flap suppression pipeline step
- Ticket digest reports with LLM analysis
- Zabbix WAN monitor and gap analysis
- Kiosk is_deleted filter fixes
- Datto RMM ping target enrichment
- Entity sync soft-delete detection
2026-03-17 07:39:55 -04:00
|
|
|
|
} else if (config.sync_type === 'ticket-digest-daily') {
|
|
|
|
|
|
await this.getTicketDigestService().run('daily');
|
|
|
|
|
|
} else if (config.sync_type === 'ticket-digest-weekly') {
|
|
|
|
|
|
await this.getTicketDigestService().run('weekly');
|
|
|
|
|
|
} else if (config.sync_type === 'ticket-digest-monthly') {
|
|
|
|
|
|
await this.getTicketDigestService().run('monthly');
|
feat: Veeam VSPC backup integration - sync, compliance, UI
- Database: 7 Veeam tables + backup_type_udf column on configuration_items
- API Client: VSPC REST API v3 client with pagination, rate limiting, Bearer auth
- Sync Service: full/incremental sync for orgs, servers, repos, jobs, agent jobs, workloads
- Scheduler: veeam-incremental (30min) and veeam-full (daily 2AM) schedules
- Compliance Engine: cross-references Autotask config items vs Veeam workloads
- API Endpoints: backup-status, companies, workloads, jobs, repos, compliance, sync
- UI: Backup Status page with Overview + Contract Compliance tabs
- Navigation: added Backup Status link with HardDrive icon
- Docker: added VEEAM_VSPC_URL and VEEAM_VSPC_API_KEY env vars to compose
2026-02-11 21:04:28 -05:00
|
|
|
|
} else if (config.sync_type === 'incremental') {
|
2026-01-26 10:29:31 -05:00
|
|
|
|
await this.syncService.incrementalSync('scheduled');
|
2026-01-26 10:24:58 -05:00
|
|
|
|
} else {
|
2026-01-26 10:29:31 -05:00
|
|
|
|
await this.syncService.fullSync('scheduled', config.years_back || 2);
|
2026-01-26 10:24:58 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Update success status
|
|
|
|
|
|
await postgresClient.query(
|
|
|
|
|
|
`UPDATE sync_schedules
|
|
|
|
|
|
SET last_status = 'success', last_error = NULL, updated_at = NOW()
|
|
|
|
|
|
WHERE id = $1`,
|
|
|
|
|
|
[config.id]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const duration = Date.now() - startTime.getTime();
|
|
|
|
|
|
console.log(`[SCHEDULER] Scheduled sync ${config.name} completed successfully in ${duration}ms`);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
|
|
|
|
console.error(`[SCHEDULER] Scheduled sync ${config.name} failed:`, errorMessage);
|
|
|
|
|
|
|
|
|
|
|
|
// Update failure status
|
|
|
|
|
|
await postgresClient.query(
|
|
|
|
|
|
`UPDATE sync_schedules
|
|
|
|
|
|
SET last_status = 'failed', last_error = $2, updated_at = NOW()
|
|
|
|
|
|
WHERE id = $1`,
|
|
|
|
|
|
[config.id, errorMessage]
|
|
|
|
|
|
);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.runningJobs.delete(config.id);
|
|
|
|
|
|
|
|
|
|
|
|
// Update next run time
|
|
|
|
|
|
this.updateNextRunTime(config.id, config.cron_expression);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Calculate and update next run time
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async updateNextRunTime(scheduleId: string, cronExpression: string): Promise<void> {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const nextRun = this.getNextRunTime(cronExpression);
|
|
|
|
|
|
if (nextRun) {
|
|
|
|
|
|
await postgresClient.query(
|
|
|
|
|
|
'UPDATE sync_schedules SET next_run = $2 WHERE id = $1',
|
|
|
|
|
|
[scheduleId, nextRun]
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error(`[SCHEDULER] Failed to update next run time for ${scheduleId}:`, error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Get next run time for a cron expression
|
|
|
|
|
|
*/
|
|
|
|
|
|
private getNextRunTime(cronExpression: string): Date | null {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// Parse cron expression and calculate next run
|
|
|
|
|
|
// This is a simplified calculation - node-cron doesn't expose this directly
|
|
|
|
|
|
const parts = cronExpression.split(' ');
|
|
|
|
|
|
if (parts.length !== 5) return null;
|
|
|
|
|
|
|
|
|
|
|
|
const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
|
|
|
|
|
|
|
|
|
|
|
|
const now = new Date();
|
|
|
|
|
|
const next = new Date(now);
|
|
|
|
|
|
|
|
|
|
|
|
// Simple calculation for common patterns
|
|
|
|
|
|
if (minute !== '*') next.setMinutes(parseInt(minute));
|
|
|
|
|
|
if (hour !== '*') next.setHours(parseInt(hour));
|
|
|
|
|
|
|
|
|
|
|
|
// If time has passed today, move to next occurrence
|
|
|
|
|
|
if (next <= now) {
|
|
|
|
|
|
if (dayOfWeek !== '*') {
|
|
|
|
|
|
// Weekly schedule
|
|
|
|
|
|
const targetDay = parseInt(dayOfWeek);
|
|
|
|
|
|
const currentDay = next.getDay();
|
|
|
|
|
|
const daysToAdd = targetDay >= currentDay ? targetDay - currentDay : 7 - currentDay + targetDay;
|
|
|
|
|
|
next.setDate(next.getDate() + daysToAdd);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Daily schedule
|
|
|
|
|
|
next.setDate(next.getDate() + 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
next.setSeconds(0);
|
|
|
|
|
|
next.setMilliseconds(0);
|
|
|
|
|
|
|
|
|
|
|
|
return next;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('[SCHEDULER] Error calculating next run time:', error);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Get all schedules
|
|
|
|
|
|
*/
|
|
|
|
|
|
async getSchedules(): Promise<ScheduleStatus[]> {
|
|
|
|
|
|
const result = await postgresClient.query(
|
|
|
|
|
|
'SELECT * FROM sync_schedules ORDER BY id'
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const schedules = result.rows as ScheduleConfig[];
|
|
|
|
|
|
|
|
|
|
|
|
return schedules.map(config => ({
|
|
|
|
|
|
config,
|
|
|
|
|
|
isRunning: this.runningJobs.has(config.id),
|
|
|
|
|
|
isValid: cron.validate(config.cron_expression),
|
|
|
|
|
|
nextRun: config.next_run || undefined,
|
|
|
|
|
|
}));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Get a specific schedule
|
|
|
|
|
|
*/
|
|
|
|
|
|
async getSchedule(scheduleId: string): Promise<ScheduleStatus | null> {
|
|
|
|
|
|
const result = await postgresClient.query(
|
|
|
|
|
|
'SELECT * FROM sync_schedules WHERE id = $1',
|
|
|
|
|
|
[scheduleId]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (result.rows.length === 0) return null;
|
|
|
|
|
|
|
|
|
|
|
|
const config = result.rows[0] as ScheduleConfig;
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
config,
|
|
|
|
|
|
isRunning: this.runningJobs.has(config.id),
|
|
|
|
|
|
isValid: cron.validate(config.cron_expression),
|
|
|
|
|
|
nextRun: config.next_run || undefined,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Update a schedule
|
|
|
|
|
|
*/
|
|
|
|
|
|
async updateSchedule(
|
|
|
|
|
|
scheduleId: string,
|
|
|
|
|
|
updates: Partial<Pick<ScheduleConfig, 'name' | 'description' | 'cron_expression' | 'sync_type' | 'years_back' | 'is_enabled'>>
|
|
|
|
|
|
): Promise<ScheduleConfig> {
|
|
|
|
|
|
// Validate cron expression if provided
|
|
|
|
|
|
if (updates.cron_expression && !cron.validate(updates.cron_expression)) {
|
|
|
|
|
|
throw new Error(`Invalid cron expression: ${updates.cron_expression}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Build update query
|
|
|
|
|
|
const fields: string[] = [];
|
|
|
|
|
|
const values: any[] = [];
|
|
|
|
|
|
let paramIndex = 1;
|
|
|
|
|
|
|
|
|
|
|
|
if (updates.name !== undefined) {
|
|
|
|
|
|
fields.push(`name = $${paramIndex++}`);
|
|
|
|
|
|
values.push(updates.name);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (updates.description !== undefined) {
|
|
|
|
|
|
fields.push(`description = $${paramIndex++}`);
|
|
|
|
|
|
values.push(updates.description);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (updates.cron_expression !== undefined) {
|
|
|
|
|
|
fields.push(`cron_expression = $${paramIndex++}`);
|
|
|
|
|
|
values.push(updates.cron_expression);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (updates.sync_type !== undefined) {
|
|
|
|
|
|
fields.push(`sync_type = $${paramIndex++}`);
|
|
|
|
|
|
values.push(updates.sync_type);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (updates.years_back !== undefined) {
|
|
|
|
|
|
fields.push(`years_back = $${paramIndex++}`);
|
|
|
|
|
|
values.push(updates.years_back);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (updates.is_enabled !== undefined) {
|
|
|
|
|
|
fields.push(`is_enabled = $${paramIndex++}`);
|
|
|
|
|
|
values.push(updates.is_enabled);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fields.push(`updated_at = NOW()`);
|
|
|
|
|
|
values.push(scheduleId);
|
|
|
|
|
|
|
|
|
|
|
|
const query = `
|
|
|
|
|
|
UPDATE sync_schedules
|
|
|
|
|
|
SET ${fields.join(', ')}
|
|
|
|
|
|
WHERE id = $${paramIndex}
|
|
|
|
|
|
RETURNING *
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
const result = await postgresClient.query(query, values);
|
|
|
|
|
|
const config = result.rows[0] as ScheduleConfig;
|
|
|
|
|
|
|
|
|
|
|
|
// Restart the schedule if it's enabled
|
|
|
|
|
|
if (config.is_enabled) {
|
|
|
|
|
|
this.startSchedule(config);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.stopSchedule(scheduleId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return config;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Create a new schedule
|
|
|
|
|
|
*/
|
|
|
|
|
|
async createSchedule(
|
|
|
|
|
|
schedule: Omit<ScheduleConfig, 'created_at' | 'updated_at' | 'last_run' | 'next_run' | 'last_status' | 'last_error'>
|
|
|
|
|
|
): Promise<ScheduleConfig> {
|
|
|
|
|
|
// Validate cron expression
|
|
|
|
|
|
if (!cron.validate(schedule.cron_expression)) {
|
|
|
|
|
|
throw new Error(`Invalid cron expression: ${schedule.cron_expression}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const query = `
|
|
|
|
|
|
INSERT INTO sync_schedules (id, name, description, cron_expression, sync_type, years_back, is_enabled)
|
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
|
|
|
|
RETURNING *
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
const result = await postgresClient.query(query, [
|
|
|
|
|
|
schedule.id,
|
|
|
|
|
|
schedule.name,
|
|
|
|
|
|
schedule.description,
|
|
|
|
|
|
schedule.cron_expression,
|
|
|
|
|
|
schedule.sync_type,
|
|
|
|
|
|
schedule.years_back || null,
|
|
|
|
|
|
schedule.is_enabled,
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
const config = result.rows[0] as ScheduleConfig;
|
|
|
|
|
|
|
|
|
|
|
|
// Start the schedule if enabled
|
|
|
|
|
|
if (config.is_enabled) {
|
|
|
|
|
|
this.startSchedule(config);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return config;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Delete a schedule
|
|
|
|
|
|
*/
|
|
|
|
|
|
async deleteSchedule(scheduleId: string): Promise<void> {
|
|
|
|
|
|
// Stop the schedule first
|
|
|
|
|
|
this.stopSchedule(scheduleId);
|
|
|
|
|
|
|
|
|
|
|
|
// Delete from database
|
|
|
|
|
|
await postgresClient.query(
|
|
|
|
|
|
'DELETE FROM sync_schedules WHERE id = $1',
|
|
|
|
|
|
[scheduleId]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`[SCHEDULER] Deleted schedule: ${scheduleId}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Manually trigger a schedule
|
|
|
|
|
|
*/
|
|
|
|
|
|
async triggerSchedule(scheduleId: string): Promise<void> {
|
|
|
|
|
|
const result = await postgresClient.query(
|
|
|
|
|
|
'SELECT * FROM sync_schedules WHERE id = $1',
|
|
|
|
|
|
[scheduleId]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (result.rows.length === 0) {
|
|
|
|
|
|
throw new Error(`Schedule not found: ${scheduleId}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const config = result.rows[0] as ScheduleConfig;
|
|
|
|
|
|
await this.executeScheduledSync(config);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Validate a cron expression
|
|
|
|
|
|
*/
|
|
|
|
|
|
validateCronExpression(expression: string): boolean {
|
|
|
|
|
|
return cron.validate(expression);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Shutdown the scheduler
|
|
|
|
|
|
*/
|
|
|
|
|
|
shutdown(): void {
|
|
|
|
|
|
console.log('[SCHEDULER] Shutting down sync scheduler...');
|
|
|
|
|
|
|
|
|
|
|
|
for (const [id, task] of this.tasks.entries()) {
|
|
|
|
|
|
task.stop();
|
|
|
|
|
|
console.log(`[SCHEDULER] Stopped schedule: ${id}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.tasks.clear();
|
|
|
|
|
|
this.runningJobs.clear();
|
|
|
|
|
|
this.initialized = false;
|
|
|
|
|
|
|
|
|
|
|
|
console.log('[SCHEDULER] Sync scheduler shut down');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Export singleton instance
|
|
|
|
|
|
export const syncScheduler = new SyncScheduler();
|
|
|
|
|
|
|
|
|
|
|
|
// Initialize on server startup (only in Node.js environment)
|
|
|
|
|
|
if (typeof window === 'undefined') {
|
|
|
|
|
|
syncScheduler.initialize().catch(error => {
|
|
|
|
|
|
console.error('[SCHEDULER] Failed to initialize on startup:', error);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|