feat: Veeam RPO analysis, comparison, ticket analysis + company teams table

- Add Veeam RPO analysis page (/veeam-analysis) and comparison page (/veeam-comparison)
- Add API routes: /api/veeam/rpo-analyze, rpo-comparison, rpo-offline-log, ticket-analysis
- Add veeam-rpo-service.ts enhancements (RPO logic, offline detection, comparison)
- Add veeam-analysis-state.ts and rmm-device-resolver.ts services
- Add migrations 065-068: company_teams, veeam_rpo_offline_log, rpo_comparison_tables, veeam_ticket_analysis
- Add backup-status page updates and nav links for new Veeam pages
- Add scripts: deactivate-cis-for-inactive-companies, workstation category updates
- Add docs: mimecast-api-guide, veeam-backup-alerting-recommendation, workstation-backup-overview, ticket-analyzer-prompt
- Minor: webhook-service, entity-sync, entity-mapper, sync-helpers, sync.ts, middleware.ts updates
This commit is contained in:
lorentz 2026-04-29 09:16:46 -04:00
parent 07067bef19
commit ea3471d38d
36 changed files with 5604 additions and 217 deletions

View file

@ -103,7 +103,7 @@ export class EntitySyncService {
// For incremental sync, filter by last sync time
// Note: Companies and Resources don't support date-based filtering in Autotask API
const supportsIncremental = entity !== EntityType.COMPANIES && entity !== EntityType.RESOURCES;
const supportsIncremental = entity !== EntityType.COMPANIES && entity !== EntityType.RESOURCES && entity !== EntityType.COMPANY_TEAMS;
if (isIncremental && supportsIncremental) {
try {
@ -415,6 +415,41 @@ export class EntitySyncService {
}
}
// After contacts sync, backfill primary_contact_id and billing_contact_id on companies
if (entity === EntityType.CONTACTS) {
try {
const primaryResult = await postgresClient.query(
`UPDATE companies c
SET primary_contact_id = (
SELECT id FROM contacts
WHERE company_id = c.id AND primary_contact = true AND is_deleted = false
ORDER BY id LIMIT 1
)
WHERE EXISTS (
SELECT 1 FROM contacts
WHERE company_id = c.id AND primary_contact = true AND is_deleted = false
)`
);
entityLogger.info('Backfilled companies.primary_contact_id', { updatedCount: (primaryResult as any).rowCount ?? 0 });
const billingResult = await postgresClient.query(
`UPDATE companies c
SET billing_contact_id = (
SELECT id FROM contacts
WHERE company_id = c.id AND billing_contact = true AND is_deleted = false
ORDER BY id LIMIT 1
)
WHERE EXISTS (
SELECT 1 FROM contacts
WHERE company_id = c.id AND billing_contact = true AND is_deleted = false
)`
);
entityLogger.info('Backfilled companies.billing_contact_id', { updatedCount: (billingResult as any).rowCount ?? 0 });
} catch (err) {
entityLogger.warn('Contact FK backfill on companies failed', { error: String(err) });
}
}
// 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.
@ -817,6 +852,13 @@ export class EntitySyncService {
return await this.syncEntity(EntityType.CONTACTS, isIncremental);
}
/**
* Sync Company Teams (TAMs, CSMs, co-managed resources assigned to a company)
*/
async syncCompanyTeams(isIncremental: boolean = false): Promise<EntitySyncStats> {
return await this.syncEntity(EntityType.COMPANY_TEAMS, isIncremental);
}
/**
* Sync Contracts
*/