feat: Autotask webhook integration, TicketNotes, Datto RMM, workflow engine, Veeam agents/alarms, AI triage, misc improvements

This commit is contained in:
lorentz 2026-02-20 10:28:15 -05:00
parent 347cf4e298
commit d7c3dc7168
74 changed files with 37844 additions and 322 deletions

View file

@ -0,0 +1,48 @@
-- Migration: Create ticket_notes table
-- Description: Stores Autotask ticket notes synced via webhooks
CREATE TABLE IF NOT EXISTS ticket_notes (
id BIGINT PRIMARY KEY,
ticket_id BIGINT NOT NULL,
title VARCHAR(500),
description TEXT,
note_type INTEGER, -- Autotask noteType picklist
publish INTEGER, -- Visibility: 1=All, 2=Internal, etc.
creator_resource_id BIGINT,
creator_type INTEGER, -- 1=Resource, 2=Contact, etc.
last_activity_date TIMESTAMP WITH TIME ZONE,
create_date_time TIMESTAMP WITH TIME ZONE,
is_deleted BOOLEAN DEFAULT false,
synced_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Indexes
CREATE INDEX IF NOT EXISTS idx_ticket_notes_ticket_id ON ticket_notes(ticket_id);
CREATE INDEX IF NOT EXISTS idx_ticket_notes_creator ON ticket_notes(creator_resource_id);
CREATE INDEX IF NOT EXISTS idx_ticket_notes_create_date ON ticket_notes(create_date_time DESC);
CREATE INDEX IF NOT EXISTS idx_ticket_notes_is_deleted ON ticket_notes(is_deleted);
-- Updated_at trigger
CREATE OR REPLACE FUNCTION update_ticket_notes_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS ticket_notes_updated_at ON ticket_notes;
CREATE TRIGGER ticket_notes_updated_at
BEFORE UPDATE ON ticket_notes
FOR EACH ROW
EXECUTE FUNCTION update_ticket_notes_updated_at();
COMMENT ON TABLE ticket_notes IS 'Autotask ticket notes synced via webhooks';
-- Ensure webhook_configs has rows for all webhook-supported entities
INSERT INTO webhook_configs (entity_type, event_types, is_active) VALUES
('TicketNotes', '["create", "update", "delete"]', true),
('ConfigurationItems', '["create", "update", "delete"]', true)
ON CONFLICT (entity_type) DO NOTHING;

View file

@ -0,0 +1,16 @@
-- Queues picklist table (synced from Autotask Ticket.queueID field)
CREATE TABLE IF NOT EXISTS queues (
value INTEGER PRIMARY KEY,
label VARCHAR(200) NOT NULL,
is_active BOOLEAN DEFAULT true,
is_system BOOLEAN DEFAULT false,
sort_order INTEGER,
parent_value INTEGER,
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_queues_is_active ON queues (is_active);

View file

@ -0,0 +1,102 @@
-- Datto RMM sync tables: sites, devices, alerts
-- Sites map to Autotask companies via autotask_company_id
CREATE TABLE IF NOT EXISTS datto_rmm_sites (
id INTEGER PRIMARY KEY,
uid TEXT UNIQUE NOT NULL,
account_uid TEXT,
name TEXT NOT NULL,
description TEXT,
notes TEXT,
on_demand BOOLEAN DEFAULT false,
autotask_company_id INTEGER REFERENCES companies(id),
autotask_company_name TEXT,
number_of_devices INTEGER DEFAULT 0,
number_of_online_devices INTEGER DEFAULT 0,
number_of_offline_devices INTEGER DEFAULT 0,
portal_url TEXT,
synced_at TIMESTAMPTZ DEFAULT NOW(),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS datto_rmm_devices (
id INTEGER PRIMARY KEY,
uid TEXT UNIQUE NOT NULL,
site_id INTEGER REFERENCES datto_rmm_sites(id),
site_uid TEXT,
site_name TEXT,
hostname TEXT,
description TEXT,
device_type_category TEXT,
device_type TEXT,
operating_system TEXT,
domain TEXT,
int_ip_address TEXT,
ext_ip_address TEXT,
last_logged_in_user TEXT,
last_seen TIMESTAMPTZ,
last_reboot TIMESTAMPTZ,
last_audit_date TIMESTAMPTZ,
creation_date TIMESTAMPTZ,
online BOOLEAN DEFAULT false,
suspended BOOLEAN DEFAULT false,
deleted BOOLEAN DEFAULT false,
reboot_required BOOLEAN DEFAULT false,
a64_bit BOOLEAN DEFAULT true,
cag_version TEXT,
display_version TEXT,
antivirus_product TEXT,
antivirus_status TEXT,
patch_status TEXT,
patches_approved_pending INTEGER DEFAULT 0,
patches_not_approved INTEGER DEFAULT 0,
patches_installed INTEGER DEFAULT 0,
software_status TEXT,
portal_url TEXT,
web_remote_url TEXT,
warranty_date TIMESTAMPTZ,
snmp_enabled BOOLEAN DEFAULT false,
device_class TEXT,
network_probe BOOLEAN DEFAULT false,
udf JSONB,
synced_at TIMESTAMPTZ DEFAULT NOW(),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS datto_rmm_alerts (
alert_uid TEXT PRIMARY KEY,
device_uid TEXT,
device_name TEXT,
site_uid TEXT,
site_name TEXT,
priority TEXT,
alert_context JSONB,
alert_monitor_info JSONB,
diagnostics TEXT,
resolved BOOLEAN DEFAULT false,
resolved_by TEXT,
resolved_on TIMESTAMPTZ,
muted BOOLEAN DEFAULT false,
ticket_number TEXT,
autoresolve_mins INTEGER,
response_actions JSONB,
timestamp TIMESTAMPTZ NOT NULL,
synced_at TIMESTAMPTZ DEFAULT NOW(),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Indexes
CREATE INDEX IF NOT EXISTS idx_datto_rmm_sites_uid ON datto_rmm_sites(uid);
CREATE INDEX IF NOT EXISTS idx_datto_rmm_sites_company ON datto_rmm_sites(autotask_company_id);
CREATE INDEX IF NOT EXISTS idx_datto_rmm_devices_uid ON datto_rmm_devices(uid);
CREATE INDEX IF NOT EXISTS idx_datto_rmm_devices_site ON datto_rmm_devices(site_id);
CREATE INDEX IF NOT EXISTS idx_datto_rmm_devices_hostname ON datto_rmm_devices(hostname);
CREATE INDEX IF NOT EXISTS idx_datto_rmm_devices_online ON datto_rmm_devices(online);
CREATE INDEX IF NOT EXISTS idx_datto_rmm_alerts_device ON datto_rmm_alerts(device_uid);
CREATE INDEX IF NOT EXISTS idx_datto_rmm_alerts_site ON datto_rmm_alerts(site_uid);
CREATE INDEX IF NOT EXISTS idx_datto_rmm_alerts_resolved ON datto_rmm_alerts(resolved);
CREATE INDEX IF NOT EXISTS idx_datto_rmm_alerts_timestamp ON datto_rmm_alerts(timestamp);
CREATE INDEX IF NOT EXISTS idx_datto_rmm_alerts_ticket ON datto_rmm_alerts(ticket_number);

View file

@ -0,0 +1,20 @@
-- Migration to create device lifecycle policies table
CREATE TABLE IF NOT EXISTS device_lifecycle_policies (
id SERIAL PRIMARY KEY,
device_type VARCHAR(255) NOT NULL UNIQUE,
expected_months INTEGER NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
is_deleted BOOLEAN DEFAULT FALSE,
deleted_at TIMESTAMP WITH TIME ZONE
);
-- Seed some default policies based on common MSP standards
INSERT INTO device_lifecycle_policies (device_type, expected_months) VALUES
('Workstation', 48), -- 4 years
('Desktop', 60), -- 5 years
('Laptop', 48), -- 4 years
('Server', 60), -- 5 years
('Network', 60), -- 5 years
('Firewall', 60) -- 5 years
ON CONFLICT (device_type) DO NOTHING;

View file

@ -0,0 +1,62 @@
-- Veeam Backup Agents (agent installs on managed machines)
CREATE TABLE IF NOT EXISTS veeam_backup_agents (
instance_uid VARCHAR(255) PRIMARY KEY,
organization_uid VARCHAR(255) REFERENCES veeam_organizations(instance_uid) ON DELETE SET NULL,
site_uid VARCHAR(255),
management_agent_uid VARCHAR(255),
name VARCHAR(500) NOT NULL,
agent_platform VARCHAR(50),
status VARCHAR(50),
management_agent_status VARCHAR(50),
operation_mode VARCHAR(50),
gui_mode VARCHAR(50),
platform VARCHAR(50),
version VARCHAR(100),
version_status VARCHAR(50),
management_mode VARCHAR(100),
installation_type VARCHAR(50),
activation_time TIMESTAMPTZ,
total_jobs_count INTEGER DEFAULT 0,
running_jobs_count INTEGER DEFAULT 0,
success_jobs_count INTEGER DEFAULT 0,
synced_at TIMESTAMPTZ DEFAULT NOW(),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Veeam VSPC Alarms (platform-level alarms, distinct from job failures)
CREATE TABLE IF NOT EXISTS veeam_alarms (
instance_uid VARCHAR(255) PRIMARY KEY,
alarm_template_uid VARCHAR(255),
repeat_count INTEGER DEFAULT 0,
object_uid VARCHAR(255),
object_type VARCHAR(100),
object_name VARCHAR(500),
object_computer_name VARCHAR(500),
organization_uid VARCHAR(255) REFERENCES veeam_organizations(instance_uid) ON DELETE SET NULL,
location_uid VARCHAR(255),
management_agent_uid VARCHAR(255),
last_activation_uid VARCHAR(255),
last_activation_time TIMESTAMPTZ,
last_activation_status VARCHAR(50),
last_activation_message TEXT,
last_activation_remark TEXT,
area VARCHAR(50),
resolved BOOLEAN DEFAULT false,
synced_at TIMESTAMPTZ DEFAULT NOW(),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Indexes
CREATE INDEX IF NOT EXISTS idx_veeam_agents_org ON veeam_backup_agents(organization_uid);
CREATE INDEX IF NOT EXISTS idx_veeam_agents_status ON veeam_backup_agents(status);
CREATE INDEX IF NOT EXISTS idx_veeam_agents_mgmt_status ON veeam_backup_agents(management_agent_status);
CREATE INDEX IF NOT EXISTS idx_veeam_agents_platform ON veeam_backup_agents(agent_platform);
CREATE INDEX IF NOT EXISTS idx_veeam_agents_synced ON veeam_backup_agents(synced_at);
CREATE INDEX IF NOT EXISTS idx_veeam_alarms_org ON veeam_alarms(organization_uid);
CREATE INDEX IF NOT EXISTS idx_veeam_alarms_status ON veeam_alarms(last_activation_status);
CREATE INDEX IF NOT EXISTS idx_veeam_alarms_resolved ON veeam_alarms(resolved);
CREATE INDEX IF NOT EXISTS idx_veeam_alarms_time ON veeam_alarms(last_activation_time);
CREATE INDEX IF NOT EXISTS idx_veeam_alarms_synced ON veeam_alarms(synced_at);

View file

@ -0,0 +1,24 @@
-- Priorities picklist table (synced from Autotask Ticket.priority field)
CREATE TABLE IF NOT EXISTS priorities (
value INTEGER PRIMARY KEY,
label VARCHAR(200) NOT NULL,
is_active BOOLEAN DEFAULT true,
sort_order INTEGER DEFAULT 0,
synced_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Ticket Categories picklist table (synced from Autotask Ticket.ticketCategory field)
CREATE TABLE IF NOT EXISTS ticket_categories (
value INTEGER PRIMARY KEY,
label VARCHAR(200) NOT NULL,
is_active BOOLEAN DEFAULT true,
sort_order INTEGER DEFAULT 0,
synced_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_priorities_active ON priorities(is_active);
CREATE INDEX IF NOT EXISTS idx_ticket_categories_active ON ticket_categories(is_active);

View file

@ -0,0 +1,349 @@
-- Workflow Engine Tables
-- Supports robotic-first ticket triage with configurable rules and AI fallback
-- Classification rules: DB-driven keyword→classification mappings
-- Replaces hardcoded keyword lists from n8n AI prompts
CREATE TABLE IF NOT EXISTS classification_rules (
id SERIAL PRIMARY KEY,
name VARCHAR(200) NOT NULL,
description TEXT,
rule_type VARCHAR(50) NOT NULL, -- 'branch_routing', 'ticket_type', 'issue_classification', 'priority', 'queue_routing'
sort_order INTEGER DEFAULT 0,
is_active BOOLEAN DEFAULT true,
-- Pattern matching
match_field VARCHAR(50) NOT NULL, -- 'title', 'description', 'title_or_description', 'ticket_category', 'policy_name', 'device_name', 'creator_resource_id', 'priority', 'ticket_type'
match_operator VARCHAR(50) NOT NULL, -- 'contains', 'starts_with', 'regex', 'equals', 'in', 'not_in'
match_value JSONB NOT NULL, -- string, string[], or regex pattern
match_case_sensitive BOOLEAN DEFAULT false,
-- Result: what to set when matched
result_field VARCHAR(50) NOT NULL, -- 'branch', 'ticket_type', 'issue_type', 'sub_issue_type', 'priority', 'queue_id', 'ticket_category'
result_value JSONB NOT NULL,
-- Optional secondary result (e.g., issue_type AND sub_issue_type together)
result_field_2 VARCHAR(50),
result_value_2 JSONB,
confidence VARCHAR(20) DEFAULT 'high', -- 'high', 'medium', 'low'
stop_on_match BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Workflow rules: configurable exclusion/inclusion filters
CREATE TABLE IF NOT EXISTS workflow_rules (
id SERIAL PRIMARY KEY,
name VARCHAR(200) NOT NULL,
description TEXT,
is_active BOOLEAN DEFAULT true,
sort_order INTEGER DEFAULT 0,
trigger_event VARCHAR(50) NOT NULL, -- 'ticket.created', 'ticket.updated'
trigger_entity VARCHAR(50) DEFAULT 'ticket',
stop_processing BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Rule conditions (AND within group, OR between groups)
CREATE TABLE IF NOT EXISTS workflow_conditions (
id SERIAL PRIMARY KEY,
rule_id INTEGER NOT NULL REFERENCES workflow_rules(id) ON DELETE CASCADE,
condition_group INTEGER DEFAULT 0,
field VARCHAR(100) NOT NULL,
operator VARCHAR(50) NOT NULL,
value JSONB NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
-- Rule actions
CREATE TABLE IF NOT EXISTS workflow_actions (
id SERIAL PRIMARY KEY,
rule_id INTEGER NOT NULL REFERENCES workflow_rules(id) ON DELETE CASCADE,
sort_order INTEGER DEFAULT 0,
action_type VARCHAR(50) NOT NULL, -- 'set_field', 'classify', 'ai_enhance', 'create_note', 'update_autotask', 'delay', 'skip'
config JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMP DEFAULT NOW()
);
-- AI prompt templates (for cases where AI is needed)
CREATE TABLE IF NOT EXISTS ai_prompt_templates (
id SERIAL PRIMARY KEY,
name VARCHAR(200) NOT NULL,
purpose VARCHAR(50) NOT NULL, -- 'title_cleanup', 'description_rewrite', 'ambiguous_classification', 'troubleshooting_steps', 'noc_format', 'soc_analysis'
system_prompt TEXT NOT NULL,
user_prompt_template TEXT NOT NULL,
provider VARCHAR(50) DEFAULT 'openai',
model VARCHAR(100) DEFAULT 'gpt-4o',
temperature DECIMAL(3,2) DEFAULT 0.3,
max_tokens INTEGER DEFAULT 4000,
is_active BOOLEAN DEFAULT true,
version INTEGER DEFAULT 1,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Workflow execution log
CREATE TABLE IF NOT EXISTS workflow_executions (
id SERIAL PRIMARY KEY,
trigger_event VARCHAR(50) NOT NULL,
entity_type VARCHAR(50) NOT NULL,
entity_id BIGINT NOT NULL,
ticket_number VARCHAR(50),
status VARCHAR(20) NOT NULL DEFAULT 'pending',
classification_method VARCHAR(20), -- 'robotic', 'ai', 'hybrid'
branch VARCHAR(20), -- 'service_desk', 'noc', 'soc'
started_at TIMESTAMP DEFAULT NOW(),
completed_at TIMESTAMP,
duration_ms INTEGER,
error_message TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
-- Execution step details
CREATE TABLE IF NOT EXISTS workflow_execution_steps (
id SERIAL PRIMARY KEY,
execution_id INTEGER NOT NULL REFERENCES workflow_executions(id) ON DELETE CASCADE,
step_name VARCHAR(100) NOT NULL,
step_order INTEGER NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
method VARCHAR(20), -- 'robotic', 'ai', 'skipped'
input_data JSONB,
output_data JSONB,
field_changes JSONB,
classification_rule_id INTEGER,
confidence VARCHAR(20),
ai_request JSONB,
ai_response TEXT,
attempt_number INTEGER DEFAULT 1,
error_message TEXT,
duration_ms INTEGER,
started_at TIMESTAMP,
completed_at TIMESTAMP
);
-- Workflow settings
CREATE TABLE IF NOT EXISTS workflow_settings (
key VARCHAR(100) PRIMARY KEY,
value JSONB NOT NULL,
description TEXT,
updated_at TIMESTAMP DEFAULT NOW()
);
INSERT INTO workflow_settings (key, value, description) VALUES
('workflow_engine_enabled', 'false', 'Master enable/disable for workflow engine'),
('default_ai_provider', '"openai"', 'Default AI provider (openai or anthropic)'),
('openai_api_key', '""', 'OpenAI API key'),
('openai_model', '"gpt-4o"', 'Default OpenAI model'),
('anthropic_api_key', '""', 'Anthropic API key'),
('anthropic_model', '"claude-sonnet-4-20250514"', 'Default Anthropic model'),
('ai_for_title_cleanup', 'true', 'Use AI to clean up messy ticket titles'),
('ai_for_description_rewrite', 'true', 'Use AI to restructure unstructured descriptions'),
('ai_for_ambiguous_classification', 'true', 'Use AI when robotic classifier has no match'),
('ai_for_troubleshooting', 'true', 'Generate AI troubleshooting steps for incidents'),
('autotask_update_delay_ms', '30000', 'Delay before writing back to Autotask (ms)'),
('max_ai_retries', '2', 'Max AI retry attempts on validation failure'),
('classification_confidence_threshold', '"medium"', 'Min confidence to skip AI (high, medium, low)'),
('log_retention_days', '90', 'Days to retain execution logs')
ON CONFLICT (key) DO NOTHING;
-- Indexes
CREATE INDEX idx_classification_rules_type ON classification_rules(rule_type, is_active, sort_order);
CREATE INDEX idx_workflow_rules_active ON workflow_rules(is_active, sort_order);
CREATE INDEX idx_workflow_conditions_rule ON workflow_conditions(rule_id);
CREATE INDEX idx_workflow_actions_rule ON workflow_actions(rule_id, sort_order);
CREATE INDEX idx_workflow_executions_entity ON workflow_executions(entity_type, entity_id);
CREATE INDEX idx_workflow_executions_status ON workflow_executions(status, created_at);
CREATE INDEX idx_workflow_executions_created ON workflow_executions(created_at);
CREATE INDEX idx_workflow_execution_steps_exec ON workflow_execution_steps(execution_id, step_order);
-- ============================================================================
-- SEED: Default workflow filter rules (port from n8n IF nodes)
-- ============================================================================
-- Rule 1: Exclude specific API users
INSERT INTO workflow_rules (name, description, is_active, sort_order, trigger_event, stop_processing) VALUES
('Exclude API Users', 'Skip tickets created by automation API users', true, 10, 'ticket.created', true);
INSERT INTO workflow_conditions (rule_id, condition_group, field, operator, value) VALUES
((SELECT id FROM workflow_rules WHERE name = 'Exclude API Users'), 0, 'person_id', 'in', '[30861575]');
INSERT INTO workflow_actions (rule_id, sort_order, action_type, config) VALUES
((SELECT id FROM workflow_rules WHERE name = 'Exclude API Users'), 0, 'skip', '{"reason": "API user exclusion"}');
-- Rule 2: Exclude specific technician creators
INSERT INTO workflow_rules (name, description, is_active, sort_order, trigger_event, stop_processing) VALUES
('Exclude Technician Creators', 'Skip tickets created by specific technicians', true, 20, 'ticket.created', true);
INSERT INTO workflow_conditions (rule_id, condition_group, field, operator, value) VALUES
((SELECT id FROM workflow_rules WHERE name = 'Exclude Technician Creators'), 0, 'creator_resource_id', 'in', '[30861471]');
INSERT INTO workflow_actions (rule_id, sort_order, action_type, config) VALUES
((SELECT id FROM workflow_rules WHERE name = 'Exclude Technician Creators'), 0, 'skip', '{"reason": "Technician creator exclusion"}');
-- Rule 3: Category filter - only process triageable categories
INSERT INTO workflow_rules (name, description, is_active, sort_order, trigger_event, stop_processing) VALUES
('Category Filter', 'Only process tickets in triageable categories (NOC, Service Desk, Triage, SOC)', true, 30, 'ticket.created', true);
INSERT INTO workflow_conditions (rule_id, condition_group, field, operator, value) VALUES
((SELECT id FROM workflow_rules WHERE name = 'Category Filter'), 0, 'ticket_category', 'not_in', '[2, 3, 159, 161]');
INSERT INTO workflow_actions (rule_id, sort_order, action_type, config) VALUES
((SELECT id FROM workflow_rules WHERE name = 'Category Filter'), 0, 'skip', '{"reason": "Ticket category not eligible for triage"}');
-- Rule 4: Company exclusions
INSERT INTO workflow_rules (name, description, is_active, sort_order, trigger_event, stop_processing) VALUES
('Company Exclusions', 'Skip tickets from excluded companies (tools-only clients)', true, 40, 'ticket.created', true);
INSERT INTO workflow_conditions (rule_id, condition_group, field, operator, value) VALUES
((SELECT id FROM workflow_rules WHERE name = 'Company Exclusions'), 0, 'company_id', 'in', '[29861409, 29783545, 29861361, 29702433]');
INSERT INTO workflow_actions (rule_id, sort_order, action_type, config) VALUES
((SELECT id FROM workflow_rules WHERE name = 'Company Exclusions'), 0, 'skip', '{"reason": "Company excluded from triage"}');
-- ============================================================================
-- SEED: Branch routing classification rules
-- ============================================================================
-- NOC branch routing
INSERT INTO classification_rules (name, rule_type, sort_order, match_field, match_operator, match_value, result_field, result_value, confidence) VALUES
('Datto RMM alerts (email)', 'branch_routing', 10, 'description', 'contains', '["alerts@rmm.datto.com", "monitor alert", "alert was triggered", "aem alert"]', 'branch', '"noc"', 'high'),
('Datto RMM alerts (title)', 'branch_routing', 20, 'title', 'contains', '["datto", "rmm"]', 'branch', '"noc"', 'high'),
('Zoom monitoring alerts', 'branch_routing', 30, 'title_or_description', 'contains', '["zoom room", "zoom monitoring", "video device disconnected", "audio device disconnected", "controller (ipad)"]', 'branch', '"noc"', 'high');
-- SOC branch routing
INSERT INTO classification_rules (name, rule_type, sort_order, match_field, match_operator, match_value, result_field, result_value, confidence) VALUES
('Phishing reports', 'branch_routing', 40, 'title', 'contains', '["phishing:", "[phish alert]"]', 'branch', '"soc"', 'high'),
('Phishing indicators (headers)', 'branch_routing', 45, 'description', 'contains', '["received-spf: fail", "dkim=fail", "dmarc=fail", "# questionable urls detected"]', 'branch', '"soc"', 'high'),
('Blumira security alerts', 'branch_routing', 50, 'title_or_description', 'contains', '["from blumira", "blumira@messages.blumira.com", "suspect |", "critical |", "informational |"]', 'branch', '"soc"', 'high'),
('Security threat keywords', 'branch_routing', 60, 'title_or_description', 'contains', '["malware detected", "ransomware", "threat detected", "suspicious activity", "security breach", "unauthorized access", "compromised account", "attack detected", "exploit", "huntress detection", "duo security alert", "brute force", "stolen credentials", "credential harvesting", "account takeover", "indicator of compromise", "security alert", "security event", "malicious link", "suspicious attachment", "incident response"]', 'branch', '"soc"', 'high');
-- Default: service_desk (handled by code when no branch_routing rule matches)
-- ============================================================================
-- SEED: Ticket type classification rules
-- ============================================================================
INSERT INTO classification_rules (name, rule_type, sort_order, match_field, match_operator, match_value, result_field, result_value, confidence) VALUES
('Incident - was working language', 'ticket_type', 10, 'title_or_description', 'contains', '["was working", "stopped working", "quit working", "suddenly stopped", "used to work", "previously worked", "it broke"]', 'ticket_type', '2', 'high'),
('Incident - error/failure language', 'ticket_type', 20, 'title_or_description', 'contains', '["not working", "won''t open", "getting error", "unable to", "failed", "can''t access", "can''t send", "can''t receive", "disconnected", "crashes", "login failure", "access denied"]', 'ticket_type', '2', 'high'),
('Service Request - request language', 'ticket_type', 30, 'title_or_description', 'contains', '["how do i", "please set up", "please install", "can i have", "need account", "new user", "new hire", "schedule", "consultation", "review my", "following up", "shipment"]', 'ticket_type', '1', 'high'),
('Service Request - setup language', 'ticket_type', 40, 'title_or_description', 'contains', '["set up", "configure", "create new", "add new", "install", "deploy", "onboarding"]', 'ticket_type', '1', 'medium');
-- ============================================================================
-- SEED: Issue classification rules (keyword → issueType + subIssueType)
-- ============================================================================
-- Category overrides (highest priority)
INSERT INTO classification_rules (name, rule_type, sort_order, match_field, match_operator, match_value, result_field, result_value, result_field_2, result_value_2, confidence) VALUES
('New User (category 127)', 'issue_classification', 1, 'ticket_category', 'equals', '127', 'issue_type', '55', 'sub_issue_type', '522', 'high'),
('User Separation (category 128)', 'issue_classification', 2, 'ticket_category', 'equals', '128', 'issue_type', '55', 'sub_issue_type', '526', 'high');
-- Email (issueType 34) - highest keyword priority per n8n rules
INSERT INTO classification_rules (name, rule_type, sort_order, match_field, match_operator, match_value, result_field, result_value, result_field_2, result_value_2, confidence) VALUES
('Email - can''t send', 'issue_classification', 10, 'title_or_description', 'contains', '["can''t send email", "unable to send email", "email not sending"]', 'issue_type', '34', 'sub_issue_type', '312', 'high'),
('Email - can''t receive', 'issue_classification', 11, 'title_or_description', 'contains', '["can''t receive email", "not receiving email", "email not coming", "can''t access email", "unable to access email", "can''t get into email"]', 'issue_type', '34', 'sub_issue_type', '313', 'high'),
('Email - spam/junk', 'issue_classification', 12, 'title_or_description', 'contains', '["spam email", "junk email", "junk mail", "too many emails"]', 'issue_type', '34', 'sub_issue_type', '314', 'high'),
('Email - phishing', 'issue_classification', 13, 'title_or_description', 'contains', '["phishing email", "phishing attempt", "suspicious email"]', 'issue_type', '34', 'sub_issue_type', '316', 'high'),
('Email - Outlook', 'issue_classification', 14, 'title_or_description', 'contains', '["outlook crash", "outlook not opening", "outlook won''t open", "outlook freezing"]', 'issue_type', '34', 'sub_issue_type', '451', 'high'),
('Email - signatures', 'issue_classification', 15, 'title_or_description', 'contains', '["email signature"]', 'issue_type', '34', 'sub_issue_type', '568', 'high'),
('Email - Mimecast release', 'issue_classification', 16, 'title_or_description', 'contains', '["mimecast release", "release email"]', 'issue_type', '34', 'sub_issue_type', '706', 'high'),
('Email - Mimecast allow', 'issue_classification', 17, 'title_or_description', 'contains', '["mimecast allow", "allow sender", "whitelist sender"]', 'issue_type', '34', 'sub_issue_type', '707', 'high'),
('Email - Mimecast block', 'issue_classification', 18, 'title_or_description', 'contains', '["mimecast block", "block sender", "blacklist sender"]', 'issue_type', '34', 'sub_issue_type', '832', 'high'),
('Email - distribution group', 'issue_classification', 19, 'title_or_description', 'contains', '["distribution group", "distribution list", "email group"]', 'issue_type', '34', 'sub_issue_type', '771', 'high'),
('Email - shared mailbox', 'issue_classification', 20, 'title_or_description', 'contains', '["shared mailbox", "shared email"]', 'issue_type', '34', 'sub_issue_type', '814', 'high'),
('Email - forwarding', 'issue_classification', 21, 'title_or_description', 'contains', '["email forward", "forward setup", "forwarding"]', 'issue_type', '34', 'sub_issue_type', '862', 'high'),
('Email - out of office', 'issue_classification', 22, 'title_or_description', 'contains', '["out of office", "auto-reply", "vacation reply"]', 'issue_type', '34', 'sub_issue_type', '839', 'high'),
('Email - mailbox full', 'issue_classification', 23, 'title_or_description', 'contains', '["mailbox full", "mailbox size", "mailbox quota"]', 'issue_type', '34', 'sub_issue_type', '838', 'high'),
('Email - generic', 'issue_classification', 29, 'title_or_description', 'contains', '["email", "e-mail", "inbox", "mailbox"]', 'issue_type', '34', 'sub_issue_type', '317', 'medium');
-- Active Directory & Accounts (issueType 76)
INSERT INTO classification_rules (name, rule_type, sort_order, match_field, match_operator, match_value, result_field, result_value, result_field_2, result_value_2, confidence) VALUES
('AD - password reset', 'issue_classification', 30, 'title_or_description', 'contains', '["password reset", "reset password", "forgot password", "password expired"]', 'issue_type', '76', 'sub_issue_type', '759', 'high'),
('AD - account lockout', 'issue_classification', 31, 'title_or_description', 'contains', '["account locked", "locked out", "account lockout"]', 'issue_type', '76', 'sub_issue_type', '795', 'high'),
('AD - DUO lockout', 'issue_classification', 32, 'title_or_description', 'contains', '["duo locked", "duo lockout", "mfa locked"]', 'issue_type', '76', 'sub_issue_type', '758', 'high'),
('AD - file permissions', 'issue_classification', 33, 'title_or_description', 'contains', '["file permission", "folder permission", "access to folder", "shared drive access"]', 'issue_type', '76', 'sub_issue_type', '760', 'high'),
('AD - drive mapping', 'issue_classification', 34, 'title_or_description', 'contains', '["drive mapping", "mapped drive", "network drive"]', 'issue_type', '76', 'sub_issue_type', '761', 'high'),
('AD - admin rights', 'issue_classification', 35, 'title_or_description', 'contains', '["admin rights", "local admin", "administrator access"]', 'issue_type', '76', 'sub_issue_type', '828', 'high'),
('AD - 365 license', 'issue_classification', 36, 'title_or_description', 'contains', '["365 license", "office license", "microsoft license"]', 'issue_type', '76', 'sub_issue_type', '829', 'high');
-- Peripheral Connectivity (issueType 51)
INSERT INTO classification_rules (name, rule_type, sort_order, match_field, match_operator, match_value, result_field, result_value, result_field_2, result_value_2, confidence) VALUES
('Peripheral - printer/printing', 'issue_classification', 40, 'title_or_description', 'contains', '["printer", "printing", "print job", "can''t print"]', 'issue_type', '51', 'sub_issue_type', '377', 'high'),
('Peripheral - scanner', 'issue_classification', 41, 'title_or_description', 'contains', '["scanner", "scanning", "can''t scan"]', 'issue_type', '51', 'sub_issue_type', '550', 'high'),
('Peripheral - scan to email', 'issue_classification', 42, 'title_or_description', 'contains', '["scan to email", "scan-to-email"]', 'issue_type', '51', 'sub_issue_type', '551', 'high'),
('Peripheral - docking station', 'issue_classification', 43, 'title_or_description', 'contains', '["docking station", "dock", "undocking"]', 'issue_type', '51', 'sub_issue_type', '769', 'high'),
('Peripheral - display/monitor', 'issue_classification', 44, 'title_or_description', 'contains', '["monitor", "display", "screen", "external display"]', 'issue_type', '51', 'sub_issue_type', '745', 'medium'),
('Peripheral - audio', 'issue_classification', 45, 'title_or_description', 'contains', '["audio", "speakers", "headset", "microphone", "sound"]', 'issue_type', '51', 'sub_issue_type', '804', 'medium'),
('Peripheral - fax', 'issue_classification', 46, 'title_or_description', 'contains', '["fax", "faxing"]', 'issue_type', '51', 'sub_issue_type', '847', 'high');
-- Software (issueType 46)
INSERT INTO classification_rules (name, rule_type, sort_order, match_field, match_operator, match_value, result_field, result_value, result_field_2, result_value_2, confidence) VALUES
('Software - MS Teams', 'issue_classification', 50, 'title_or_description', 'contains', '["microsoft teams", "ms teams", "teams meeting", "teams call"]', 'issue_type', '46', 'sub_issue_type', '798', 'high'),
('Software - SharePoint', 'issue_classification', 51, 'title_or_description', 'contains', '["sharepoint"]', 'issue_type', '46', 'sub_issue_type', '850', 'high'),
('Software - OneDrive', 'issue_classification', 52, 'title_or_description', 'contains', '["onedrive", "one drive"]', 'issue_type', '46', 'sub_issue_type', '345', 'high'),
('Software - Excel', 'issue_classification', 53, 'title_or_description', 'contains', '["excel", "spreadsheet"]', 'issue_type', '46', 'sub_issue_type', '842', 'high'),
('Software - Word', 'issue_classification', 54, 'title_or_description', 'contains', '["microsoft word", "ms word"]', 'issue_type', '46', 'sub_issue_type', '799', 'high'),
('Software - PowerPoint', 'issue_classification', 55, 'title_or_description', 'contains', '["powerpoint", "power point"]', 'issue_type', '46', 'sub_issue_type', '800', 'high'),
('Software - MS Project', 'issue_classification', 56, 'title_or_description', 'contains', '["ms project", "microsoft project"]', 'issue_type', '46', 'sub_issue_type', '836', 'high'),
('Software - Adobe', 'issue_classification', 57, 'title_or_description', 'contains', '["adobe", "acrobat", "photoshop", "illustrator"]', 'issue_type', '46', 'sub_issue_type', '541', 'high'),
('Software - Chrome', 'issue_classification', 58, 'title_or_description', 'contains', '["google chrome", "chrome browser"]', 'issue_type', '46', 'sub_issue_type', '542', 'high'),
('Software - QuickBooks', 'issue_classification', 59, 'title_or_description', 'contains', '["quickbooks", "quick books"]', 'issue_type', '46', 'sub_issue_type', '583', 'high'),
('Software - Bitwarden', 'issue_classification', 60, 'title_or_description', 'contains', '["bitwarden"]', 'issue_type', '46', 'sub_issue_type', '851', 'high'),
('Software - MS Office generic', 'issue_classification', 69, 'title_or_description', 'contains', '["microsoft sway", "onenote", "one note", "visio", "publisher", "ms access", "microsoft forms", "planner"]', 'issue_type', '46', 'sub_issue_type', '332', 'high');
-- Networking (issueType 25)
INSERT INTO classification_rules (name, rule_type, sort_order, match_field, match_operator, match_value, result_field, result_value, result_field_2, result_value_2, confidence) VALUES
('Network - WiFi', 'issue_classification', 70, 'title_or_description', 'contains', '["wifi", "wi-fi", "wireless", "wireless network"]', 'issue_type', '25', 'sub_issue_type', '548', 'high'),
('Network - VPN', 'issue_classification', 71, 'title_or_description', 'contains', '["vpn", "site to site"]', 'issue_type', '25', 'sub_issue_type', '358', 'medium'),
('Network - internet down', 'issue_classification', 72, 'title_or_description', 'contains', '["internet down", "internet outage", "no internet", "internet not working"]', 'issue_type', '25', 'sub_issue_type', '260', 'high'),
('Network - firewall', 'issue_classification', 73, 'title_or_description', 'contains', '["firewall"]', 'issue_type', '25', 'sub_issue_type', '244', 'high');
-- Hardware (issueType 31)
INSERT INTO classification_rules (name, rule_type, sort_order, match_field, match_operator, match_value, result_field, result_value, result_field_2, result_value_2, confidence) VALUES
('Hardware - slow PC', 'issue_classification', 80, 'title_or_description', 'contains', '["slow computer", "slow pc", "computer slow", "pc slow", "running slow"]', 'issue_type', '31', 'sub_issue_type', '560', 'high'),
('Hardware - laptop', 'issue_classification', 81, 'title_or_description', 'contains', '["laptop issue", "laptop problem", "laptop broken"]', 'issue_type', '31', 'sub_issue_type', '561', 'medium'),
('Hardware - desktop', 'issue_classification', 82, 'title_or_description', 'contains', '["desktop issue", "desktop problem", "desktop broken", "computer won''t turn on"]', 'issue_type', '31', 'sub_issue_type', '248', 'medium');
-- Remote Access (issueType 63)
INSERT INTO classification_rules (name, rule_type, sort_order, match_field, match_operator, match_value, result_field, result_value, result_field_2, result_value_2, confidence) VALUES
('Remote Access - VPN/Sophos', 'issue_classification', 90, 'title_or_description', 'contains', '["sophos vpn", "sophos connect"]', 'issue_type', '63', 'sub_issue_type', '494', 'high'),
('Remote Access - RDP', 'issue_classification', 91, 'title_or_description', 'contains', '["rdp", "remote desktop", "rds gateway"]', 'issue_type', '63', 'sub_issue_type', '495', 'high'),
('Remote Access - generic', 'issue_classification', 99, 'title_or_description', 'contains', '["remote access", "work from home", "remote work"]', 'issue_type', '63', 'sub_issue_type', '770', 'medium');
-- Collaboration (issueType 42) - for phone/video specific
INSERT INTO classification_rules (name, rule_type, sort_order, match_field, match_operator, match_value, result_field, result_value, result_field_2, result_value_2, confidence) VALUES
('Collaboration - Nextiva', 'issue_classification', 100, 'title_or_description', 'contains', '["nextiva", "phone system"]', 'issue_type', '42', 'sub_issue_type', '563', 'high'),
('Collaboration - Teams Room', 'issue_classification', 101, 'title_or_description', 'contains', '["teams room", "conference room teams"]', 'issue_type', '42', 'sub_issue_type', '810', 'high');
-- Security (issueType 59)
INSERT INTO classification_rules (name, rule_type, sort_order, match_field, match_operator, match_value, result_field, result_value, result_field_2, result_value_2, confidence) VALUES
('Security - phishing', 'issue_classification', 110, 'title_or_description', 'contains', '["phishing", "phish"]', 'issue_type', '59', 'sub_issue_type', '910', 'high'),
('Security - malware', 'issue_classification', 111, 'title_or_description', 'contains', '["malware", "virus", "trojan"]', 'issue_type', '59', 'sub_issue_type', '554', 'high'),
('Security - ransomware', 'issue_classification', 112, 'title_or_description', 'contains', '["ransomware"]', 'issue_type', '59', 'sub_issue_type', '911', 'high'),
('Security - compromised account', 'issue_classification', 113, 'title_or_description', 'contains', '["compromised account", "account compromised", "account hacked"]', 'issue_type', '59', 'sub_issue_type', '552', 'high'),
('Security - 2FA/MFA setup', 'issue_classification', 114, 'title_or_description', 'contains', '["2fa setup", "mfa setup", "two factor", "multi factor"]', 'issue_type', '59', 'sub_issue_type', '860', 'high'),
('Security - DUO enrollment', 'issue_classification', 115, 'title_or_description', 'contains', '["duo enrollment", "duo setup", "enroll duo"]', 'issue_type', '59', 'sub_issue_type', '813', 'high');
-- ============================================================================
-- SEED: Priority classification rules
-- ============================================================================
INSERT INTO classification_rules (name, rule_type, sort_order, match_field, match_operator, match_value, result_field, result_value, confidence) VALUES
-- Category-based overrides (highest priority)
('Priority - User Separation = Critical', 'priority', 1, 'ticket_category', 'equals', '128', 'priority', '4', 'high'),
('Priority - New User = Minor Service', 'priority', 2, 'ticket_category', 'equals', '127', 'priority', '8', 'high'),
-- Security events
('Priority - Security keywords = Security Event', 'priority', 10, 'title_or_description', 'contains', '["security breach", "ransomware", "malware detected", "compromised", "attack"]', 'priority', '7', 'high'),
-- Impact-based
('Priority - Multiple users affected = Critical', 'priority', 20, 'title_or_description', 'contains', '["multiple users", "everyone", "all users", "company-wide", "entire office"]', 'priority', '4', 'high'),
-- Simple service requests
('Priority - How-to/consultation = Minor Service', 'priority', 30, 'title_or_description', 'contains', '["how do i", "how to", "schedule meeting", "consultation", "question about", "following up"]', 'priority', '8', 'medium');
-- ============================================================================
-- SEED: Queue routing classification rules
-- ============================================================================
INSERT INTO classification_rules (name, rule_type, sort_order, match_field, match_operator, match_value, result_field, result_value, confidence) VALUES
-- Critical priority override
('Queue - Critical = Level 2', 'queue_routing', 1, 'priority', 'equals', '4', 'queue_id', '29682969', 'high'),
-- Device name patterns (workstation)
('Queue - DT devices = Level 1', 'queue_routing', 10, 'title_or_description', 'regex', '"\\bDT-?\\d{2,}"', 'queue_id', '29682833', 'high'),
('Queue - LT devices = Level 1', 'queue_routing', 11, 'title_or_description', 'regex', '"\\bLT-?\\d{2,}"', 'queue_id', '29682833', 'high'),
('Queue - SP devices = Level 1', 'queue_routing', 12, 'title_or_description', 'regex', '"\\bSP-?\\d{2,}"', 'queue_id', '29682833', 'high'),
-- Policy name patterns
('Queue - Workstation policy = Level 1', 'queue_routing', 20, 'title_or_description', 'contains', '["windows workstation", "workstations"]', 'queue_id', '29682833', 'high'),
('Queue - Server policy = Level 2', 'queue_routing', 21, 'title_or_description', 'contains', '["windows server", "hypervisor", "esxi", "hyper-v"]', 'queue_id', '29682969', 'high');