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

@ -0,0 +1,42 @@
-- Migration 065: Create company_teams table and add primary/billing contact FKs to companies
-- Company Teams: maps resources (TAMs, CSMs, etc.) to companies
CREATE TABLE IF NOT EXISTS company_teams (
id BIGINT PRIMARY KEY,
company_id BIGINT REFERENCES companies(id) ON DELETE CASCADE,
resource_id BIGINT,
is_associated_as_comanaged BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
synced_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_deleted BOOLEAN DEFAULT false,
deleted_at TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_company_teams_company_id ON company_teams(company_id);
CREATE INDEX IF NOT EXISTS idx_company_teams_resource_id ON company_teams(resource_id);
-- Add primary and billing contact FK columns to companies
ALTER TABLE companies
ADD COLUMN IF NOT EXISTS primary_contact_id BIGINT REFERENCES contacts(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS billing_contact_id BIGINT REFERENCES contacts(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS idx_companies_primary_contact ON companies(primary_contact_id) WHERE primary_contact_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_companies_billing_contact ON companies(billing_contact_id) WHERE billing_contact_id IS NOT NULL;
-- Backfill from existing contacts data
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 primary_contact_id IS NULL;
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 billing_contact_id IS NULL;

View file

@ -0,0 +1,21 @@
-- Tracks every RPO check cycle where a workstation job was suppressed
-- because the device was offline (last_seen older than its backup interval).
-- Used for auditing, reporting, and the Claude Code skill.
CREATE TABLE IF NOT EXISTS veeam_rpo_offline_log (
id SERIAL PRIMARY KEY,
job_instance_uid VARCHAR(255) NOT NULL,
job_name TEXT NOT NULL,
org_name TEXT NOT NULL,
rmm_hostname TEXT NOT NULL,
rmm_site_name TEXT NOT NULL,
device_type_category TEXT NOT NULL,
rmm_last_seen TIMESTAMPTZ,
hours_offline NUMERIC(10,2) NOT NULL,
backup_interval_hours INTEGER NOT NULL,
checked_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_veeam_rpo_offline_log_job ON veeam_rpo_offline_log(job_instance_uid);
CREATE INDEX IF NOT EXISTS idx_veeam_rpo_offline_log_checked_at ON veeam_rpo_offline_log(checked_at);
CREATE INDEX IF NOT EXISTS idx_veeam_rpo_offline_log_hostname ON veeam_rpo_offline_log(rmm_hostname);

View file

@ -0,0 +1,73 @@
-- Veeam RPO shadow-mode tables for the parallel validation period.
-- The RPO service runs without creating real Autotask tickets; all actions
-- are logged here so they can be compared against Datto RMM ticket output.
-- Shadow log: one row per check cycle per job for every action the live service
-- *would* have taken (would_create, would_escalate, would_resolve, would_skip_too_old).
-- Offline suppressions continue to go to veeam_rpo_offline_log (migration 066).
CREATE TABLE IF NOT EXISTS veeam_rpo_shadow_log (
id SERIAL PRIMARY KEY,
job_instance_uid VARCHAR(255) NOT NULL,
job_name TEXT NOT NULL,
org_name TEXT NOT NULL,
rmm_hostname TEXT,
rmm_site_name TEXT,
action TEXT NOT NULL,
-- 'would_create' | 'would_escalate' | 'would_resolve' | 'would_skip_too_old'
priority_level TEXT, -- medium | high | critical (null for would_resolve)
hours_overdue INTEGER, -- null for would_resolve
failure_category TEXT,
checked_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_veeam_rpo_shadow_log_job ON veeam_rpo_shadow_log(job_instance_uid);
CREATE INDEX IF NOT EXISTS idx_veeam_rpo_shadow_log_checked ON veeam_rpo_shadow_log(checked_at);
CREATE INDEX IF NOT EXISTS idx_veeam_rpo_shadow_log_hostname ON veeam_rpo_shadow_log(rmm_hostname);
CREATE INDEX IF NOT EXISTS idx_veeam_rpo_shadow_log_action ON veeam_rpo_shadow_log(action);
-- Shadow ticket state: mirrors veeam_rpo_tickets but without Autotask ticket IDs.
-- Tracks open/resolved state so the shadow service can distinguish would_create
-- from would_escalate across check cycles.
CREATE TABLE IF NOT EXISTS veeam_rpo_shadow_tickets (
job_instance_uid VARCHAR(255) PRIMARY KEY,
job_name TEXT NOT NULL,
org_name TEXT NOT NULL,
rmm_hostname TEXT,
priority_level TEXT NOT NULL,
hours_overdue INTEGER NOT NULL,
failure_category TEXT,
opened_at TIMESTAMPTZ DEFAULT NOW(),
resolved_at TIMESTAMPTZ,
last_checked_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_veeam_rpo_shadow_tickets_open ON veeam_rpo_shadow_tickets(resolved_at) WHERE resolved_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_veeam_rpo_shadow_tickets_hostname ON veeam_rpo_shadow_tickets(rmm_hostname);
-- Datto RMM backup tickets captured from Autotask webhooks.
-- Populated whenever a ticket.created event arrives that matches the Datto
-- backup monitor signature (issue_type=38, Veeam-related title, not [Veeam RPO]).
-- Updated on ticket.updated events to track resolution.
CREATE TABLE IF NOT EXISTS veeam_rpo_datto_tickets (
id SERIAL PRIMARY KEY,
at_ticket_id BIGINT UNIQUE NOT NULL,
at_ticket_number TEXT,
title TEXT NOT NULL,
company_id BIGINT,
company_name TEXT,
extracted_hostname TEXT, -- parsed from title (e.g. "DT061" from "DT061 - Veeam...")
issue_type INTEGER,
sub_issue_type INTEGER,
status INTEGER,
priority INTEGER,
ticket_created_at TIMESTAMPTZ,
ticket_resolved_at TIMESTAMPTZ,
captured_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_veeam_rpo_datto_tickets_company ON veeam_rpo_datto_tickets(company_id);
CREATE INDEX IF NOT EXISTS idx_veeam_rpo_datto_tickets_hostname ON veeam_rpo_datto_tickets(extracted_hostname);
CREATE INDEX IF NOT EXISTS idx_veeam_rpo_datto_tickets_created ON veeam_rpo_datto_tickets(ticket_created_at);
CREATE INDEX IF NOT EXISTS idx_veeam_rpo_datto_tickets_resolved ON veeam_rpo_datto_tickets(ticket_resolved_at) WHERE ticket_resolved_at IS NULL;

View file

@ -0,0 +1,31 @@
CREATE TABLE IF NOT EXISTS veeam_ticket_analysis (
id SERIAL PRIMARY KEY,
ticket_number TEXT NOT NULL UNIQUE,
ticket_id BIGINT,
company_id BIGINT,
company_name TEXT,
device_hostname TEXT,
ticket_created_at TIMESTAMPTZ,
ticket_closed_at TIMESTAMPTZ,
same_day_close BOOLEAN NOT NULL DEFAULT false,
hours_worked NUMERIC(10,2) NOT NULL DEFAULT 0,
note_count INTEGER NOT NULL DEFAULT 0,
-- LLM classification
problem_category TEXT NOT NULL,
resolution_type TEXT NOT NULL,
skills_required TEXT[] NOT NULL DEFAULT '{}',
complexity TEXT NOT NULL,
device_was_offline BOOLEAN,
backup_completed_before_tech BOOLEAN,
preventable BOOLEAN,
work_summary TEXT,
recommended_procedure TEXT,
-- meta
model TEXT,
analyzed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_vta_ticket_number ON veeam_ticket_analysis(ticket_number);
CREATE INDEX IF NOT EXISTS idx_vta_category ON veeam_ticket_analysis(problem_category);
CREATE INDEX IF NOT EXISTS idx_vta_company ON veeam_ticket_analysis(company_id);
CREATE INDEX IF NOT EXISTS idx_vta_created ON veeam_ticket_analysis(ticket_created_at);