feat: IT Glue integration, workflow engine, pipelines, Zabbix WAN, notification channels, backup status UI improvements, nav alignment fixes
This commit is contained in:
parent
ed6c4a8b65
commit
19605f82aa
97 changed files with 17080 additions and 304 deletions
21
migrations/031_create_datto_rmm_webhook_logs.sql
Normal file
21
migrations/031_create_datto_rmm_webhook_logs.sql
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
-- Datto RMM Webhook Logs
|
||||
-- Generic capture table for incoming Datto RMM webhook payloads
|
||||
-- No processing logic yet — store everything raw for inspection
|
||||
|
||||
CREATE TABLE IF NOT EXISTS datto_rmm_webhook_logs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
received_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
source_ip VARCHAR(45),
|
||||
user_agent TEXT,
|
||||
headers JSONB,
|
||||
payload JSONB,
|
||||
raw_body TEXT,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'received', -- received, processed, failed
|
||||
notes TEXT,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_datto_rmm_webhook_logs_received ON datto_rmm_webhook_logs(received_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_datto_rmm_webhook_logs_status ON datto_rmm_webhook_logs(status);
|
||||
|
||||
COMMENT ON TABLE datto_rmm_webhook_logs IS 'Raw capture of all incoming Datto RMM webhook payloads for inspection';
|
||||
60
migrations/032_add_webhook_fields_to_datto_rmm_alerts.sql
Normal file
60
migrations/032_add_webhook_fields_to_datto_rmm_alerts.sql
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
-- Add missing fields to datto_rmm_alerts for webhook ingestion
|
||||
-- Matches Datto RMM webhook payload field names exactly
|
||||
|
||||
-- Alert metadata
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS alert_category TEXT;
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS alert_type TEXT;
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS alert_message_en TEXT;
|
||||
|
||||
-- Device info
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS device_id TEXT;
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS device_hostname TEXT;
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS device_ip TEXT;
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS device_os TEXT;
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS device_description TEXT;
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS last_user TEXT;
|
||||
|
||||
-- Site info
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS site_id TEXT;
|
||||
|
||||
-- Platform
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS platform TEXT;
|
||||
|
||||
-- Triggered flag from webhook (True = alert fired, False = resolved)
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS triggered TEXT;
|
||||
|
||||
-- Device UDFs 1–29 (individual columns matching webhook field names)
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS device_udf1 TEXT;
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS device_udf2 TEXT;
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS device_udf3 TEXT;
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS device_udf4 TEXT;
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS device_udf5 TEXT;
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS device_udf6 TEXT;
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS device_udf7 TEXT;
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS device_udf8 TEXT;
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS device_udf9 TEXT;
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS device_udf10 TEXT;
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS device_udf11 TEXT;
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS device_udf12 TEXT;
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS device_udf13 TEXT;
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS device_udf14 TEXT;
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS device_udf15 TEXT;
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS device_udf16 TEXT;
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS device_udf17 TEXT;
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS device_udf18 TEXT;
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS device_udf19 TEXT;
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS device_udf20 TEXT;
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS device_udf21 TEXT;
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS device_udf22 TEXT;
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS device_udf23 TEXT;
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS device_udf24 TEXT;
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS device_udf25 TEXT;
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS device_udf26 TEXT;
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS device_udf27 TEXT;
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS device_udf28 TEXT;
|
||||
ALTER TABLE datto_rmm_alerts ADD COLUMN IF NOT EXISTS device_udf29 TEXT;
|
||||
|
||||
-- Indexes for new fields
|
||||
CREATE INDEX IF NOT EXISTS idx_datto_rmm_alerts_alert_type ON datto_rmm_alerts(alert_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_datto_rmm_alerts_alert_category ON datto_rmm_alerts(alert_category);
|
||||
CREATE INDEX IF NOT EXISTS idx_datto_rmm_alerts_device_hostname ON datto_rmm_alerts(device_hostname);
|
||||
157
migrations/033_create_pipeline_engine_tables.sql
Normal file
157
migrations/033_create_pipeline_engine_tables.sql
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
-- Pipeline Engine Tables
|
||||
-- Webhook-triggered automation pipelines with multi-step execution,
|
||||
-- notification channels, and human-in-the-loop approvals.
|
||||
|
||||
-- ============================================================================
|
||||
-- Notification Channels — UI-configured notification providers
|
||||
-- ============================================================================
|
||||
CREATE TABLE IF NOT EXISTS notification_channels (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
channel_type VARCHAR(20) NOT NULL, -- 'teams', 'telegram', 'ntfy', 'webhook'
|
||||
config JSONB NOT NULL DEFAULT '{}', -- type-specific: webhook_url, bot_token, chat_id, topic, etc.
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================================================
|
||||
-- Webhook Pipelines — workflow definitions
|
||||
-- ============================================================================
|
||||
CREATE TABLE IF NOT EXISTS webhook_pipelines (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
description TEXT,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
trigger_source VARCHAR(50) NOT NULL, -- 'datto_rmm', 'autotask', 'veeam', 'manual'
|
||||
trigger_conditions JSONB NOT NULL DEFAULT '[]', -- array of {field, operator, value}
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================================================
|
||||
-- Pipeline Steps — ordered actions within a pipeline
|
||||
-- ============================================================================
|
||||
CREATE TABLE IF NOT EXISTS pipeline_steps (
|
||||
id SERIAL PRIMARY KEY,
|
||||
pipeline_id INTEGER NOT NULL REFERENCES webhook_pipelines(id) ON DELETE CASCADE,
|
||||
step_order INTEGER NOT NULL,
|
||||
step_type VARCHAR(50) NOT NULL, -- 'filter','transform','enrich_device','create_ticket','notify','approval','rmm_quick_job', etc.
|
||||
name VARCHAR(200) NOT NULL,
|
||||
config JSONB NOT NULL DEFAULT '{}',
|
||||
on_failure VARCHAR(20) DEFAULT 'stop', -- 'continue', 'stop', 'skip_to'
|
||||
skip_to_step INTEGER,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
timeout_ms INTEGER,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================================================
|
||||
-- Pipeline Executions — runtime log
|
||||
-- ============================================================================
|
||||
CREATE TABLE IF NOT EXISTS pipeline_executions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
pipeline_id INTEGER NOT NULL REFERENCES webhook_pipelines(id) ON DELETE CASCADE,
|
||||
trigger_source VARCHAR(50) NOT NULL,
|
||||
trigger_payload JSONB,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- 'pending','running','waiting','completed','failed','skipped'
|
||||
current_step INTEGER,
|
||||
context JSONB NOT NULL DEFAULT '{}', -- accumulated data from steps
|
||||
started_at TIMESTAMP DEFAULT NOW(),
|
||||
completed_at TIMESTAMP,
|
||||
duration_ms INTEGER,
|
||||
error_message TEXT,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================================================
|
||||
-- Pipeline Execution Steps — per-step log
|
||||
-- ============================================================================
|
||||
CREATE TABLE IF NOT EXISTS pipeline_execution_steps (
|
||||
id SERIAL PRIMARY KEY,
|
||||
execution_id INTEGER NOT NULL REFERENCES pipeline_executions(id) ON DELETE CASCADE,
|
||||
step_order INTEGER NOT NULL,
|
||||
step_type VARCHAR(50) NOT NULL,
|
||||
step_name VARCHAR(200),
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- 'pending','running','completed','failed','waiting','skipped'
|
||||
input_data JSONB,
|
||||
output_data JSONB,
|
||||
started_at TIMESTAMP,
|
||||
completed_at TIMESTAMP,
|
||||
duration_ms INTEGER,
|
||||
error_message TEXT
|
||||
);
|
||||
|
||||
-- ============================================================================
|
||||
-- Approval Requests — human-in-the-loop
|
||||
-- ============================================================================
|
||||
CREATE TABLE IF NOT EXISTS approval_requests (
|
||||
id SERIAL PRIMARY KEY,
|
||||
execution_id INTEGER NOT NULL REFERENCES pipeline_executions(id) ON DELETE CASCADE,
|
||||
step_order INTEGER NOT NULL,
|
||||
channel_id INTEGER REFERENCES notification_channels(id),
|
||||
message TEXT NOT NULL,
|
||||
options JSONB NOT NULL DEFAULT '["Approve","Reject"]',
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- 'pending','approved','rejected','timeout'
|
||||
responded_by TEXT,
|
||||
responded_at TIMESTAMP,
|
||||
response_data JSONB,
|
||||
expires_at TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================================================
|
||||
-- Indexes
|
||||
-- ============================================================================
|
||||
CREATE INDEX IF NOT EXISTS idx_notification_channels_type ON notification_channels(channel_type, is_active);
|
||||
CREATE INDEX IF NOT EXISTS idx_webhook_pipelines_source ON webhook_pipelines(trigger_source, is_active, sort_order);
|
||||
CREATE INDEX IF NOT EXISTS idx_pipeline_steps_pipeline ON pipeline_steps(pipeline_id, step_order);
|
||||
CREATE INDEX IF NOT EXISTS idx_pipeline_executions_pipeline ON pipeline_executions(pipeline_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_pipeline_executions_status ON pipeline_executions(status, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_pipeline_execution_steps_exec ON pipeline_execution_steps(execution_id, step_order);
|
||||
CREATE INDEX IF NOT EXISTS idx_approval_requests_exec ON approval_requests(execution_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_approval_requests_status ON approval_requests(status, expires_at);
|
||||
|
||||
-- ============================================================================
|
||||
-- SEED: Example pipeline — "RMM Alert → Autotask Ticket"
|
||||
-- ============================================================================
|
||||
INSERT INTO webhook_pipelines (name, description, is_active, trigger_source, trigger_conditions, sort_order) VALUES
|
||||
('RMM Alert → Autotask Ticket',
|
||||
'Creates an Autotask ticket when a Datto RMM alert fires (triggered=True). Enriches with device and company data.',
|
||||
false,
|
||||
'datto_rmm',
|
||||
'[{"field": "triggered", "operator": "equals", "value": "True"}]',
|
||||
10);
|
||||
|
||||
INSERT INTO pipeline_steps (pipeline_id, step_order, step_type, name, config) VALUES
|
||||
((SELECT id FROM webhook_pipelines WHERE name = 'RMM Alert → Autotask Ticket'), 1, 'transform', 'Extract alert fields', '{
|
||||
"mappings": {
|
||||
"alert_type": "{{trigger.alert_type}}",
|
||||
"alert_priority": "{{trigger.alert_priority}}",
|
||||
"alert_message": "{{trigger.alert_message_en}}",
|
||||
"device_hostname": "{{trigger.device_hostname}}",
|
||||
"device_uid": "{{trigger.device_uid}}",
|
||||
"site_name": "{{trigger.site_name}}",
|
||||
"site_uid": "{{trigger.site_uid}}",
|
||||
"device_ip": "{{trigger.device_ip}}",
|
||||
"device_os": "{{trigger.device_os}}",
|
||||
"last_user": "{{trigger.last_user}}"
|
||||
}
|
||||
}'),
|
||||
((SELECT id FROM webhook_pipelines WHERE name = 'RMM Alert → Autotask Ticket'), 2, 'enrich_company', 'Lookup company from site', '{
|
||||
"lookup_by": "site_name",
|
||||
"source_field": "{{context.site_name}}"
|
||||
}'),
|
||||
((SELECT id FROM webhook_pipelines WHERE name = 'RMM Alert → Autotask Ticket'), 3, 'create_ticket', 'Create Autotask ticket', '{
|
||||
"template": {
|
||||
"title": "[RMM {{context.alert_type}}] {{context.device_hostname}} - {{context.alert_message}}",
|
||||
"description": "Datto RMM Alert\n\nType: {{context.alert_type}}\nPriority: {{context.alert_priority}}\nDevice: {{context.device_hostname}} ({{context.device_ip}})\nOS: {{context.device_os}}\nSite: {{context.site_name}}\nLast User: {{context.last_user}}\n\nMessage:\n{{context.alert_message}}",
|
||||
"companyID": "{{context.company_id}}",
|
||||
"ticketType": 2,
|
||||
"ticketCategory": 3,
|
||||
"priority": 1,
|
||||
"queueID": 29682833
|
||||
}
|
||||
}');
|
||||
152
migrations/034_seed_veeam_backup_failure_pipeline.sql
Normal file
152
migrations/034_seed_veeam_backup_failure_pipeline.sql
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
-- Migration 034: Seed Veeam Backup Failure Diagnostic Pipeline
|
||||
-- A comprehensive pipeline that enriches, diagnoses, and creates smart tickets
|
||||
-- for Veeam backup failure alerts from Datto RMM.
|
||||
|
||||
INSERT INTO webhook_pipelines (name, description, is_active, trigger_source, trigger_conditions, sort_order)
|
||||
VALUES (
|
||||
'Veeam Backup Failure → Smart Diagnostic Ticket',
|
||||
'When RMM detects a Veeam backup failure: enrich from VSPC + DB, run diagnostics via quick job, AI-analyze all findings, create rich Autotask ticket, notify Teams.',
|
||||
false,
|
||||
'datto_rmm',
|
||||
'[
|
||||
{"field": "triggered", "operator": "equals", "value": "True"},
|
||||
{"field": "alert_message_en", "operator": "contains", "value": "Veeam"}
|
||||
]'::jsonb,
|
||||
10
|
||||
)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Get the pipeline ID
|
||||
DO $$
|
||||
DECLARE
|
||||
pid INTEGER;
|
||||
BEGIN
|
||||
SELECT id INTO pid FROM webhook_pipelines WHERE name = 'Veeam Backup Failure → Smart Diagnostic Ticket' LIMIT 1;
|
||||
|
||||
IF pid IS NULL THEN
|
||||
RAISE NOTICE 'Pipeline not found, skipping step insertion';
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
-- Step 1: Extract alert fields from RMM payload
|
||||
INSERT INTO pipeline_steps (pipeline_id, step_order, step_type, name, config, on_failure)
|
||||
VALUES (pid, 1, 'transform', 'Extract alert fields', '{
|
||||
"mappings": {
|
||||
"device_hostname": "{{trigger.device_hostname}}",
|
||||
"device_uid": "{{trigger.device_uid}}",
|
||||
"site_name": "{{trigger.site_name}}",
|
||||
"site_uid": "{{trigger.site_uid}}",
|
||||
"alert_type": "{{trigger.alert_type}}",
|
||||
"alert_message": "{{trigger.alert_message_en}}",
|
||||
"alert_uid": "{{trigger.alert_uid}}",
|
||||
"alert_priority": "{{trigger.alert_priority}}",
|
||||
"device_ip": "{{trigger.device_ip}}",
|
||||
"device_os": "{{trigger.device_os}}",
|
||||
"last_user": "{{trigger.last_user}}"
|
||||
}
|
||||
}'::jsonb, 'stop');
|
||||
|
||||
-- Step 2: Enrich device from local RMM DB
|
||||
INSERT INTO pipeline_steps (pipeline_id, step_order, step_type, name, config, on_failure)
|
||||
VALUES (pid, 2, 'enrich_device', 'Lookup device details', '{
|
||||
"lookup_by": "device_uid",
|
||||
"source_field": "{{context.device_uid}}"
|
||||
}'::jsonb, 'continue');
|
||||
|
||||
-- Step 3: Enrich company from site name
|
||||
INSERT INTO pipeline_steps (pipeline_id, step_order, step_type, name, config, on_failure)
|
||||
VALUES (pid, 3, 'enrich_company', 'Lookup company from site', '{
|
||||
"lookup_by": "site_name",
|
||||
"source_field": "{{context.site_name}}"
|
||||
}'::jsonb, 'continue');
|
||||
|
||||
-- Step 4: Query VSPC for backup status
|
||||
INSERT INTO pipeline_steps (pipeline_id, step_order, step_type, name, config, on_failure)
|
||||
VALUES (pid, 4, 'enrich_vspc', 'VSPC backup status lookup', '{
|
||||
"lookup_by": "device_name",
|
||||
"source_field": "{{context.device_hostname}}"
|
||||
}'::jsonb, 'continue');
|
||||
|
||||
-- Step 5: DB query — backup failure trend (last 7 days)
|
||||
INSERT INTO pipeline_steps (pipeline_id, step_order, step_type, name, config, on_failure)
|
||||
VALUES (pid, 5, 'db_query', 'Backup failure trend (7 days)', '{
|
||||
"query": "SELECT status, COUNT(*) as count, MAX(last_run) as latest FROM veeam_backup_agent_jobs WHERE LOWER(name) LIKE LOWER($1) AND last_run > NOW() - INTERVAL ''7 days'' GROUP BY status ORDER BY count DESC",
|
||||
"params": ["%{{context.device_hostname}}%"],
|
||||
"output_key": "backup_trend",
|
||||
"single_row": false
|
||||
}'::jsonb, 'continue');
|
||||
|
||||
-- Step 6: DB query — recent RMM alerts for this device (pattern detection)
|
||||
INSERT INTO pipeline_steps (pipeline_id, step_order, step_type, name, config, on_failure)
|
||||
VALUES (pid, 6, 'db_query', 'Recent alerts for device (7 days)', '{
|
||||
"query": "SELECT alert_type, priority, message, resolved, created_at FROM datto_rmm_alerts WHERE device_uid = $1 AND created_at > NOW() - INTERVAL ''7 days'' ORDER BY created_at DESC LIMIT 20",
|
||||
"params": ["{{context.device_uid}}"],
|
||||
"output_key": "recent_alerts",
|
||||
"single_row": false
|
||||
}'::jsonb, 'continue');
|
||||
|
||||
-- Step 7: Run diagnostic script on device via RMM Quick Job
|
||||
-- NOTE: component_uid must be set after uploading the script to Datto RMM
|
||||
INSERT INTO pipeline_steps (pipeline_id, step_order, step_type, name, config, on_failure)
|
||||
VALUES (pid, 7, 'rmm_quick_job', 'Run Veeam diagnostic script', '{
|
||||
"device_uid": "{{context.device_uid}}",
|
||||
"component_uid": "REPLACE_WITH_COMPONENT_UID",
|
||||
"job_name": "Veeam Backup Diagnostic - {{context.device_hostname}}"
|
||||
}'::jsonb, 'continue');
|
||||
|
||||
-- Step 8: Wait for job results
|
||||
INSERT INTO pipeline_steps (pipeline_id, step_order, step_type, name, config, on_failure)
|
||||
VALUES (pid, 8, 'delay', 'Wait for diagnostic script', '{
|
||||
"seconds": 60
|
||||
}'::jsonb, 'continue');
|
||||
|
||||
-- Step 9: Get job results
|
||||
INSERT INTO pipeline_steps (pipeline_id, step_order, step_type, name, config, on_failure)
|
||||
VALUES (pid, 9, 'rmm_get_job_results', 'Retrieve diagnostic results', '{
|
||||
"job_uid": "{{context.job_uid}}",
|
||||
"device_uid": "{{context.device_uid}}"
|
||||
}'::jsonb, 'continue');
|
||||
|
||||
-- Step 10: AI analysis of all collected data
|
||||
INSERT INTO pipeline_steps (pipeline_id, step_order, step_type, name, config, on_failure)
|
||||
VALUES (pid, 10, 'ai_analyze', 'AI root cause analysis', '{
|
||||
"system_prompt": "You are a senior systems engineer specializing in Veeam Backup & Replication and Windows Server infrastructure. Analyze the provided diagnostic data and give a clear, actionable assessment.",
|
||||
"prompt": "A Veeam backup failure alert was triggered for device {{context.device_hostname}} at site {{context.site_name}}.\n\n## Alert Details\n- Type: {{context.alert_type}}\n- Message: {{context.alert_message}}\n- Priority: {{context.alert_priority}}\n- Device OS: {{context.device_os}}\n- Last User: {{context.last_user}}\n\n## VSPC Backup Status\n{{context.vspc_summary}}\n\n## Backup Trend (Last 7 Days)\n{{context.backup_trend}}\n\n## Recent RMM Alerts for This Device\n{{context.recent_alerts}}\n\n## On-Device Diagnostic Script Results\n{{context.job_results}}\n\nBased on ALL of this data:\n1. What is the most likely ROOT CAUSE of the backup failure?\n2. Is this a recurring issue or a one-time failure?\n3. What are the specific REMEDIATION STEPS (in order of priority)?\n4. Is this CRITICAL (needs immediate attention) or can it wait?\n5. Are there any related issues that should be addressed?",
|
||||
"max_tokens": 1500
|
||||
}'::jsonb, 'continue');
|
||||
|
||||
-- Step 11: Create rich Autotask ticket
|
||||
INSERT INTO pipeline_steps (pipeline_id, step_order, step_type, name, config, on_failure)
|
||||
VALUES (pid, 11, 'create_ticket', 'Create diagnostic ticket', '{
|
||||
"template": {
|
||||
"title": "[Veeam Backup Failure] {{context.device_hostname}} - {{context.site_name}}",
|
||||
"description": "## Automated Veeam Backup Failure Diagnostic\n\n**Device:** {{context.device_hostname}} ({{context.device_ip}})\n**Site:** {{context.site_name}}\n**Alert:** {{context.alert_message}}\n**OS:** {{context.device_os}}\n**Last User:** {{context.last_user}}\n\n---\n\n## VSPC Backup Status\n{{context.vspc_summary}}\n\n**Last Successful Backup:** {{context.vspc_last_success}} ({{context.vspc_hours_since_success}}h ago)\n**Failed Jobs:** {{context.vspc_failed_job_count}}\n**Active Alarms:** {{context.vspc_alarm_count}}\n**Restore Points:** {{context.vspc_restore_points}}\n\n---\n\n## AI Root Cause Analysis\n{{context.ai_response}}\n\n---\n\n## On-Device Diagnostics\n{{context.job_results}}\n\n---\n\n## Backup Trend (7 Days)\n{{context.backup_trend}}\n\n## Recent Device Alerts\n{{context.recent_alerts}}\n\n---\n*This ticket was automatically generated by Pulse Pipeline Engine with full diagnostic enrichment.*",
|
||||
"companyID": "{{context.company_id}}",
|
||||
"ticketType": 2,
|
||||
"priority": 2,
|
||||
"status": 1,
|
||||
"queueID": 29682833
|
||||
}
|
||||
}'::jsonb, 'stop');
|
||||
|
||||
-- Step 12: Add AI analysis as internal note
|
||||
INSERT INTO pipeline_steps (pipeline_id, step_order, step_type, name, config, on_failure)
|
||||
VALUES (pid, 12, 'create_note', 'Add AI analysis note', '{
|
||||
"ticket_id": "{{context.ticket_id}}",
|
||||
"title": "AI Root Cause Analysis",
|
||||
"body": "{{context.ai_response}}",
|
||||
"note_type": 1,
|
||||
"publish": 1
|
||||
}'::jsonb, 'continue');
|
||||
|
||||
-- Step 13: Notify Teams
|
||||
-- NOTE: channel_id must be set after creating a notification channel
|
||||
INSERT INTO pipeline_steps (pipeline_id, step_order, step_type, name, config, on_failure)
|
||||
VALUES (pid, 13, 'notify', 'Notify Teams channel', '{
|
||||
"channel_id": 1,
|
||||
"title": "Veeam Backup Failure: {{context.device_hostname}}",
|
||||
"message": "**Device:** {{context.device_hostname}} @ {{context.site_name}}\n**Alert:** {{context.alert_message}}\n**Last Success:** {{context.vspc_last_success}} ({{context.vspc_hours_since_success}}h ago)\n**Failed Jobs:** {{context.vspc_failed_job_count}}\n**Ticket:** #{{context.ticket_number}}\n\n**AI Assessment:**\n{{context.ai_response}}"
|
||||
}'::jsonb, 'continue');
|
||||
|
||||
RAISE NOTICE 'Veeam Backup Failure pipeline seeded with 13 steps (pipeline_id=%)', pid;
|
||||
END $$;
|
||||
50
migrations/035_update_veeam_pipeline_b2_fetch.sql
Normal file
50
migrations/035_update_veeam_pipeline_b2_fetch.sql
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
-- Migration 035: Update Veeam Backup Failure pipeline to use B2 storage for diagnostic results
|
||||
-- Inserts a fetch_b2_result step after rmm_get_job_results and updates references
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
v_pipeline_id INTEGER;
|
||||
BEGIN
|
||||
SELECT id INTO v_pipeline_id FROM webhook_pipelines WHERE name = 'Veeam Backup Failure → Smart Diagnostic Ticket';
|
||||
IF v_pipeline_id IS NULL THEN
|
||||
RAISE NOTICE 'Veeam pipeline not found — skipping';
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
-- Shift steps 10-13 → 11-14 to make room for the new fetch_b2_result step at position 10
|
||||
-- Update in reverse order to avoid unique constraint conflicts on (pipeline_id, step_order)
|
||||
UPDATE pipeline_steps SET step_order = 14 WHERE pipeline_id = v_pipeline_id AND step_order = 13;
|
||||
UPDATE pipeline_steps SET step_order = 13 WHERE pipeline_id = v_pipeline_id AND step_order = 12;
|
||||
UPDATE pipeline_steps SET step_order = 12 WHERE pipeline_id = v_pipeline_id AND step_order = 11;
|
||||
UPDATE pipeline_steps SET step_order = 11 WHERE pipeline_id = v_pipeline_id AND step_order = 10;
|
||||
|
||||
-- Insert fetch_b2_result step at position 10
|
||||
INSERT INTO pipeline_steps (pipeline_id, step_order, step_type, name, config)
|
||||
VALUES (
|
||||
v_pipeline_id, 10, 'fetch_b2_result', 'Download diagnostic results from B2',
|
||||
'{
|
||||
"object_key": "{{context.job_results}}",
|
||||
"output_key": "diagnostic_results"
|
||||
}'::jsonb
|
||||
);
|
||||
|
||||
-- Update AI analyze step (now step 11): replace {{context.job_results}} with {{context.diagnostic_results}}
|
||||
UPDATE pipeline_steps
|
||||
SET config = jsonb_set(
|
||||
config,
|
||||
'{prompt}',
|
||||
to_jsonb(replace(config->>'prompt', '{{context.job_results}}', '{{context.diagnostic_results}}'))
|
||||
)
|
||||
WHERE pipeline_id = v_pipeline_id AND step_type = 'ai_analyze';
|
||||
|
||||
-- Update create_ticket step (now step 12): replace {{context.job_results}} with {{context.diagnostic_results}}
|
||||
UPDATE pipeline_steps
|
||||
SET config = jsonb_set(
|
||||
config,
|
||||
'{template,description}',
|
||||
to_jsonb(replace(config->'template'->>'description', '{{context.job_results}}', '{{context.diagnostic_results}}'))
|
||||
)
|
||||
WHERE pipeline_id = v_pipeline_id AND step_type = 'create_ticket';
|
||||
|
||||
RAISE NOTICE 'Veeam pipeline updated: inserted fetch_b2_result at step 10, shifted steps 10-13 → 11-14, updated references';
|
||||
END $$;
|
||||
204
migrations/036_create_ticket_workflow_tables.sql
Normal file
204
migrations/036_create_ticket_workflow_tables.sql
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
-- Ticket Workflow Engine Tables
|
||||
-- Refactors the monolithic workflow engine into a flexible, pipeline-like system
|
||||
-- with per-workflow and per-step on/off switches, visual step editing, and
|
||||
-- extensible step executors following the webhook pipeline engine pattern.
|
||||
|
||||
-- ============================================================================
|
||||
-- Ticket Workflows — workflow definitions (analogous to webhook_pipelines)
|
||||
-- ============================================================================
|
||||
CREATE TABLE IF NOT EXISTS ticket_workflows (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
description TEXT,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
trigger_event VARCHAR(50) NOT NULL, -- 'ticket.created', 'ticket.updated'
|
||||
trigger_conditions JSONB NOT NULL DEFAULT '[]', -- array of {field, operator, value}
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================================================
|
||||
-- Ticket Workflow Steps — steps within a workflow (analogous to pipeline_steps)
|
||||
-- ============================================================================
|
||||
CREATE TABLE IF NOT EXISTS ticket_workflow_steps (
|
||||
id SERIAL PRIMARY KEY,
|
||||
workflow_id INTEGER NOT NULL REFERENCES ticket_workflows(id) ON DELETE CASCADE,
|
||||
step_order INTEGER NOT NULL,
|
||||
step_type VARCHAR(50) NOT NULL, -- 'classify','validate','ai_classify','ai_title','ai_troubleshooting','delay','update_ticket','filter'
|
||||
name VARCHAR(200) NOT NULL,
|
||||
config JSONB NOT NULL DEFAULT '{}', -- step-specific configuration
|
||||
on_failure VARCHAR(20) DEFAULT 'continue', -- 'continue', 'stop', 'skip_to'
|
||||
skip_to_step INTEGER,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
condition JSONB, -- optional condition to execute this step: {field, operator, value}
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================================================
|
||||
-- Ticket Workflow Executions — execution log (replaces workflow_executions)
|
||||
-- ============================================================================
|
||||
CREATE TABLE IF NOT EXISTS ticket_workflow_executions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
workflow_id INTEGER NOT NULL REFERENCES ticket_workflows(id) ON DELETE CASCADE,
|
||||
ticket_id BIGINT NOT NULL,
|
||||
ticket_number VARCHAR(50),
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- 'pending','running','completed','failed','skipped'
|
||||
classification_method VARCHAR(20), -- 'robotic', 'ai', 'hybrid'
|
||||
branch VARCHAR(20), -- 'service_desk', 'noc', 'soc'
|
||||
context JSONB NOT NULL DEFAULT '{}', -- accumulated data from steps
|
||||
field_changes JSONB, -- final changes applied to ticket
|
||||
started_at TIMESTAMP DEFAULT NOW(),
|
||||
completed_at TIMESTAMP,
|
||||
duration_ms INTEGER,
|
||||
error_message TEXT,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================================================
|
||||
-- Ticket Workflow Execution Steps — per-step audit trail (replaces workflow_execution_steps)
|
||||
-- ============================================================================
|
||||
CREATE TABLE IF NOT EXISTS ticket_workflow_execution_steps (
|
||||
id SERIAL PRIMARY KEY,
|
||||
execution_id INTEGER NOT NULL REFERENCES ticket_workflow_executions(id) ON DELETE CASCADE,
|
||||
step_order INTEGER NOT NULL,
|
||||
step_type VARCHAR(50) NOT NULL,
|
||||
step_name VARCHAR(200),
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- 'pending','running','completed','failed','skipped'
|
||||
input_data JSONB,
|
||||
output_data JSONB,
|
||||
started_at TIMESTAMP,
|
||||
completed_at TIMESTAMP,
|
||||
duration_ms INTEGER,
|
||||
error_message TEXT
|
||||
);
|
||||
|
||||
-- ============================================================================
|
||||
-- Indexes
|
||||
-- ============================================================================
|
||||
CREATE INDEX IF NOT EXISTS idx_ticket_workflows_event ON ticket_workflows(trigger_event, is_active, sort_order);
|
||||
CREATE INDEX IF NOT EXISTS idx_ticket_workflow_steps_workflow ON ticket_workflow_steps(workflow_id, step_order);
|
||||
CREATE INDEX IF NOT EXISTS idx_ticket_workflow_executions_workflow ON ticket_workflow_executions(workflow_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_ticket_workflow_executions_ticket ON ticket_workflow_executions(ticket_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_ticket_workflow_executions_status ON ticket_workflow_executions(status, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_ticket_workflow_execution_steps_exec ON ticket_workflow_execution_steps(execution_id, step_order);
|
||||
|
||||
-- ============================================================================
|
||||
-- SEED: "Ticket Triage" Workflow (ports current hardcoded workflow-engine.ts logic)
|
||||
-- ============================================================================
|
||||
INSERT INTO ticket_workflows (name, description, is_active, trigger_event, trigger_conditions, sort_order) VALUES
|
||||
('Ticket Triage',
|
||||
'Automatically classifies new tickets using robotic keyword matching, validates the classification, enhances with AI when needed, and writes back to Autotask.',
|
||||
true,
|
||||
'ticket.created',
|
||||
'[
|
||||
{
|
||||
"field": "ticket_category",
|
||||
"operator": "in",
|
||||
"value": [2, 3, 159, 161]
|
||||
},
|
||||
{
|
||||
"field": "creator_resource_id",
|
||||
"operator": "not_in",
|
||||
"value": [30861471]
|
||||
},
|
||||
{
|
||||
"field": "person_id",
|
||||
"operator": "not_in",
|
||||
"value": [30861575]
|
||||
},
|
||||
{
|
||||
"field": "company_id",
|
||||
"operator": "not_in",
|
||||
"value": [29861409, 29783545, 29861361, 29702433]
|
||||
}
|
||||
]',
|
||||
10);
|
||||
|
||||
-- Step 1: Classify - Branch Routing
|
||||
INSERT INTO ticket_workflow_steps (workflow_id, step_order, step_type, name, config, is_active) VALUES
|
||||
((SELECT id FROM ticket_workflows WHERE name = 'Ticket Triage'), 1, 'classify', 'Branch Routing', '{
|
||||
"rule_type": "branch_routing",
|
||||
"result_field": "branch",
|
||||
"default_value": "service_desk"
|
||||
}', true);
|
||||
|
||||
-- Step 2: Classify - Ticket Type
|
||||
INSERT INTO ticket_workflow_steps (workflow_id, step_order, step_type, name, config, is_active) VALUES
|
||||
((SELECT id FROM ticket_workflows WHERE name = 'Ticket Triage'), 2, 'classify', 'Ticket Type', '{
|
||||
"rule_type": "ticket_type",
|
||||
"result_field": "ticket_type"
|
||||
}', true);
|
||||
|
||||
-- Step 3: Classify - Issue Classification
|
||||
INSERT INTO ticket_workflow_steps (workflow_id, step_order, step_type, name, config, is_active) VALUES
|
||||
((SELECT id FROM ticket_workflows WHERE name = 'Ticket Triage'), 3, 'classify', 'Issue Classification', '{
|
||||
"rule_type": "issue_classification",
|
||||
"result_field": "issue_type",
|
||||
"result_field_2": "sub_issue_type"
|
||||
}', true);
|
||||
|
||||
-- Step 4: Classify - Priority
|
||||
INSERT INTO ticket_workflow_steps (workflow_id, step_order, step_type, name, config, is_active) VALUES
|
||||
((SELECT id FROM ticket_workflows WHERE name = 'Ticket Triage'), 4, 'classify', 'Priority', '{
|
||||
"rule_type": "priority",
|
||||
"result_field": "priority"
|
||||
}', true);
|
||||
|
||||
-- Step 5: Classify - Queue Routing
|
||||
INSERT INTO ticket_workflow_steps (workflow_id, step_order, step_type, name, config, is_active) VALUES
|
||||
((SELECT id FROM ticket_workflows WHERE name = 'Ticket Triage'), 5, 'classify', 'Queue Routing', '{
|
||||
"rule_type": "queue_routing",
|
||||
"result_field": "queue_id"
|
||||
}', true);
|
||||
|
||||
-- Step 6: Validate Classification
|
||||
INSERT INTO ticket_workflow_steps (workflow_id, step_order, step_type, name, config, is_active, on_failure) VALUES
|
||||
((SELECT id FROM ticket_workflows WHERE name = 'Ticket Triage'), 6, 'validate', 'Validate Classification', '{
|
||||
"required_fields": []
|
||||
}', true, 'continue');
|
||||
|
||||
-- Step 7: AI Classify (conditional - only if validation failed)
|
||||
INSERT INTO ticket_workflow_steps (workflow_id, step_order, step_type, name, config, is_active, condition) VALUES
|
||||
((SELECT id FROM ticket_workflows WHERE name = 'Ticket Triage'), 7, 'ai_classify', 'AI Classification', '{
|
||||
"template_purpose": "ambiguous_classification",
|
||||
"skip_if_valid": true
|
||||
}', true, '{
|
||||
"field": "context.validation.is_valid",
|
||||
"operator": "equals",
|
||||
"value": false
|
||||
}');
|
||||
|
||||
-- Step 8: AI Title Cleanup (conditional - only if title needs cleanup)
|
||||
INSERT INTO ticket_workflow_steps (workflow_id, step_order, step_type, name, config, is_active, condition) VALUES
|
||||
((SELECT id FROM ticket_workflows WHERE name = 'Ticket Triage'), 8, 'ai_title', 'AI Title Cleanup', '{
|
||||
"template_purpose": "title_cleanup"
|
||||
}', true, '{
|
||||
"field": "context.classification.ai_reasons",
|
||||
"operator": "contains",
|
||||
"value": "Title"
|
||||
}');
|
||||
|
||||
-- Step 9: Delay before Autotask update
|
||||
INSERT INTO ticket_workflow_steps (workflow_id, step_order, step_type, name, config, is_active) VALUES
|
||||
((SELECT id FROM ticket_workflows WHERE name = 'Ticket Triage'), 9, 'delay', 'Delay Before Update', '{
|
||||
"duration_ms": "{{settings.autotask_update_delay_ms}}"
|
||||
}', true);
|
||||
|
||||
-- Step 10: Update Ticket in Autotask
|
||||
INSERT INTO ticket_workflow_steps (workflow_id, step_order, step_type, name, config, is_active, on_failure) VALUES
|
||||
((SELECT id FROM ticket_workflows WHERE name = 'Ticket Triage'), 10, 'update_ticket', 'Update Autotask Ticket', '{
|
||||
"use_field_changes": true
|
||||
}', true, 'stop');
|
||||
|
||||
-- Step 11: AI Troubleshooting (conditional - only for incidents)
|
||||
INSERT INTO ticket_workflow_steps (workflow_id, step_order, step_type, name, config, is_active, condition) VALUES
|
||||
((SELECT id FROM ticket_workflows WHERE name = 'Ticket Triage'), 11, 'ai_troubleshooting', 'Generate Troubleshooting Steps', '{
|
||||
"template_purpose": "troubleshooting_steps",
|
||||
"create_note": true
|
||||
}', true, '{
|
||||
"field": "context.field_changes.ticket_type.after",
|
||||
"operator": "equals",
|
||||
"value": 2
|
||||
}');
|
||||
407
migrations/037_create_itglue_tables.sql
Normal file
407
migrations/037_create_itglue_tables.sql
Normal file
|
|
@ -0,0 +1,407 @@
|
|||
-- Migration 037: IT Glue sync tables (all prefixed itg_)
|
||||
-- Full backup of IT Glue data synced from the API
|
||||
|
||||
-- ─── Reference / Lookup Tables ───────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS itg_organization_types (
|
||||
id BIGINT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ,
|
||||
updated_at TIMESTAMPTZ,
|
||||
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS itg_organization_statuses (
|
||||
id BIGINT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ,
|
||||
updated_at TIMESTAMPTZ,
|
||||
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS itg_configuration_types (
|
||||
id BIGINT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ,
|
||||
updated_at TIMESTAMPTZ,
|
||||
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS itg_configuration_statuses (
|
||||
id BIGINT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ,
|
||||
updated_at TIMESTAMPTZ,
|
||||
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS itg_contact_types (
|
||||
id BIGINT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ,
|
||||
updated_at TIMESTAMPTZ,
|
||||
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS itg_password_categories (
|
||||
id BIGINT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ,
|
||||
updated_at TIMESTAMPTZ,
|
||||
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS itg_manufacturers (
|
||||
id BIGINT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ,
|
||||
updated_at TIMESTAMPTZ,
|
||||
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS itg_models (
|
||||
id BIGINT PRIMARY KEY,
|
||||
manufacturer_id BIGINT,
|
||||
name TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ,
|
||||
updated_at TIMESTAMPTZ,
|
||||
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS itg_operating_systems (
|
||||
id BIGINT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ,
|
||||
updated_at TIMESTAMPTZ,
|
||||
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS itg_platforms (
|
||||
id BIGINT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ,
|
||||
updated_at TIMESTAMPTZ,
|
||||
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS itg_countries (
|
||||
id BIGINT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
iso_code TEXT,
|
||||
created_at TIMESTAMPTZ,
|
||||
updated_at TIMESTAMPTZ,
|
||||
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ─── Organizations ────────────────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS itg_organizations (
|
||||
id BIGINT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
short_name TEXT,
|
||||
organization_type_id BIGINT,
|
||||
organization_type_name TEXT,
|
||||
organization_status_id BIGINT,
|
||||
organization_status_name TEXT,
|
||||
psa_integration TEXT,
|
||||
psa_id TEXT,
|
||||
sync_active BOOLEAN DEFAULT FALSE,
|
||||
primary_org BOOLEAN DEFAULT FALSE,
|
||||
quick_notes TEXT,
|
||||
description TEXT,
|
||||
alert TEXT,
|
||||
parent_id BIGINT,
|
||||
created_at TIMESTAMPTZ,
|
||||
updated_at TIMESTAMPTZ,
|
||||
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_itg_organizations_name ON itg_organizations(name);
|
||||
CREATE INDEX IF NOT EXISTS idx_itg_organizations_type ON itg_organizations(organization_type_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_itg_organizations_status ON itg_organizations(organization_status_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_itg_organizations_updated ON itg_organizations(updated_at);
|
||||
|
||||
-- ─── Locations ────────────────────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS itg_locations (
|
||||
id BIGINT PRIMARY KEY,
|
||||
organization_id BIGINT NOT NULL,
|
||||
organization_name TEXT,
|
||||
name TEXT NOT NULL,
|
||||
primary_location BOOLEAN DEFAULT FALSE,
|
||||
address_1 TEXT,
|
||||
address_2 TEXT,
|
||||
city TEXT,
|
||||
region_name TEXT,
|
||||
postal_code TEXT,
|
||||
country_name TEXT,
|
||||
phone TEXT,
|
||||
fax TEXT,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ,
|
||||
updated_at TIMESTAMPTZ,
|
||||
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_itg_locations_org ON itg_locations(organization_id);
|
||||
|
||||
-- ─── Contacts ─────────────────────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS itg_contacts (
|
||||
id BIGINT PRIMARY KEY,
|
||||
organization_id BIGINT NOT NULL,
|
||||
organization_name TEXT,
|
||||
first_name TEXT,
|
||||
last_name TEXT,
|
||||
name TEXT,
|
||||
title TEXT,
|
||||
contact_type_id BIGINT,
|
||||
contact_type_name TEXT,
|
||||
location_id BIGINT,
|
||||
important BOOLEAN DEFAULT FALSE,
|
||||
notes TEXT,
|
||||
emails JSONB DEFAULT '[]',
|
||||
phones JSONB DEFAULT '[]',
|
||||
created_at TIMESTAMPTZ,
|
||||
updated_at TIMESTAMPTZ,
|
||||
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_itg_contacts_org ON itg_contacts(organization_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_itg_contacts_name ON itg_contacts(last_name, first_name);
|
||||
|
||||
-- ─── Configurations ───────────────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS itg_configurations (
|
||||
id BIGINT PRIMARY KEY,
|
||||
organization_id BIGINT NOT NULL,
|
||||
organization_name TEXT,
|
||||
name TEXT NOT NULL,
|
||||
hostname TEXT,
|
||||
primary_ip TEXT,
|
||||
mac_address TEXT,
|
||||
serial_number TEXT,
|
||||
asset_tag TEXT,
|
||||
position TEXT,
|
||||
installed_by TEXT,
|
||||
purchased_by TEXT,
|
||||
notes TEXT,
|
||||
operating_system_notes TEXT,
|
||||
warranty_expires_at TIMESTAMPTZ,
|
||||
installed_at TIMESTAMPTZ,
|
||||
purchased_at TIMESTAMPTZ,
|
||||
end_of_life_at TIMESTAMPTZ,
|
||||
configuration_type_id BIGINT,
|
||||
configuration_type_name TEXT,
|
||||
configuration_status_id BIGINT,
|
||||
configuration_status_name TEXT,
|
||||
manufacturer_id BIGINT,
|
||||
manufacturer_name TEXT,
|
||||
model_id BIGINT,
|
||||
model_name TEXT,
|
||||
operating_system_id BIGINT,
|
||||
operating_system_name TEXT,
|
||||
location_id BIGINT,
|
||||
contact_id BIGINT,
|
||||
rmm_id TEXT,
|
||||
rmm_integration_type TEXT,
|
||||
created_at TIMESTAMPTZ,
|
||||
updated_at TIMESTAMPTZ,
|
||||
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_itg_configurations_org ON itg_configurations(organization_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_itg_configurations_hostname ON itg_configurations(hostname);
|
||||
CREATE INDEX IF NOT EXISTS idx_itg_configurations_serial ON itg_configurations(serial_number);
|
||||
CREATE INDEX IF NOT EXISTS idx_itg_configurations_name ON itg_configurations(name);
|
||||
CREATE INDEX IF NOT EXISTS idx_itg_configurations_rmm ON itg_configurations(rmm_id);
|
||||
|
||||
-- ─── Configuration Interfaces ─────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS itg_configuration_interfaces (
|
||||
id BIGINT PRIMARY KEY,
|
||||
configuration_id BIGINT NOT NULL,
|
||||
organization_id BIGINT,
|
||||
name TEXT,
|
||||
ip_address TEXT,
|
||||
mac_address TEXT,
|
||||
primary_interface BOOLEAN DEFAULT FALSE,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ,
|
||||
updated_at TIMESTAMPTZ,
|
||||
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_itg_config_interfaces_config ON itg_configuration_interfaces(configuration_id);
|
||||
|
||||
-- ─── Flexible Asset Types ─────────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS itg_flexible_asset_types (
|
||||
id BIGINT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
icon TEXT,
|
||||
enabled BOOLEAN DEFAULT TRUE,
|
||||
builtin BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ,
|
||||
updated_at TIMESTAMPTZ,
|
||||
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ─── Flexible Asset Fields ────────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS itg_flexible_asset_fields (
|
||||
id BIGINT PRIMARY KEY,
|
||||
flexible_asset_type_id BIGINT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
kind TEXT,
|
||||
hint TEXT,
|
||||
decimals INT DEFAULT 0,
|
||||
tag_type TEXT,
|
||||
required BOOLEAN DEFAULT FALSE,
|
||||
use_for_title BOOLEAN DEFAULT FALSE,
|
||||
expiration BOOLEAN DEFAULT FALSE,
|
||||
show_in_list BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ,
|
||||
updated_at TIMESTAMPTZ,
|
||||
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_itg_fa_fields_type ON itg_flexible_asset_fields(flexible_asset_type_id);
|
||||
|
||||
-- ─── Flexible Assets ──────────────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS itg_flexible_assets (
|
||||
id BIGINT PRIMARY KEY,
|
||||
organization_id BIGINT NOT NULL,
|
||||
organization_name TEXT,
|
||||
flexible_asset_type_id BIGINT NOT NULL,
|
||||
flexible_asset_type_name TEXT,
|
||||
name TEXT,
|
||||
traits JSONB DEFAULT '{}',
|
||||
archived BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ,
|
||||
updated_at TIMESTAMPTZ,
|
||||
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_itg_flexible_assets_org ON itg_flexible_assets(organization_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_itg_flexible_assets_type ON itg_flexible_assets(flexible_asset_type_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_itg_flexible_assets_name ON itg_flexible_assets(name);
|
||||
CREATE INDEX IF NOT EXISTS idx_itg_flexible_assets_updated ON itg_flexible_assets(updated_at);
|
||||
|
||||
-- ─── Password Folders ─────────────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS itg_password_folders (
|
||||
id BIGINT PRIMARY KEY,
|
||||
organization_id BIGINT NOT NULL,
|
||||
organization_name TEXT,
|
||||
name TEXT NOT NULL,
|
||||
inherited BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ,
|
||||
updated_at TIMESTAMPTZ,
|
||||
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_itg_password_folders_org ON itg_password_folders(organization_id);
|
||||
|
||||
-- ─── Passwords ────────────────────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS itg_passwords (
|
||||
id BIGINT PRIMARY KEY,
|
||||
organization_id BIGINT NOT NULL,
|
||||
organization_name TEXT,
|
||||
name TEXT NOT NULL,
|
||||
username TEXT,
|
||||
password TEXT,
|
||||
url TEXT,
|
||||
notes TEXT,
|
||||
password_category_id BIGINT,
|
||||
password_category_name TEXT,
|
||||
password_folder_id BIGINT,
|
||||
autofill_selectors TEXT,
|
||||
otp_enabled BOOLEAN DEFAULT FALSE,
|
||||
archived BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ,
|
||||
updated_at TIMESTAMPTZ,
|
||||
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_itg_passwords_org ON itg_passwords(organization_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_itg_passwords_name ON itg_passwords(name);
|
||||
|
||||
-- ─── Documents ────────────────────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS itg_documents (
|
||||
id BIGINT PRIMARY KEY,
|
||||
organization_id BIGINT NOT NULL,
|
||||
organization_name TEXT,
|
||||
name TEXT NOT NULL,
|
||||
content TEXT,
|
||||
draft BOOLEAN DEFAULT FALSE,
|
||||
archived BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ,
|
||||
updated_at TIMESTAMPTZ,
|
||||
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_itg_documents_org ON itg_documents(organization_id);
|
||||
|
||||
-- ─── Domains ──────────────────────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS itg_domains (
|
||||
id BIGINT PRIMARY KEY,
|
||||
organization_id BIGINT NOT NULL,
|
||||
organization_name TEXT,
|
||||
name TEXT NOT NULL,
|
||||
screenshot TEXT,
|
||||
whois_updated_at TIMESTAMPTZ,
|
||||
expires_at TIMESTAMPTZ,
|
||||
registrar_name TEXT,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ,
|
||||
updated_at TIMESTAMPTZ,
|
||||
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_itg_domains_org ON itg_domains(organization_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_itg_domains_name ON itg_domains(name);
|
||||
CREATE INDEX IF NOT EXISTS idx_itg_domains_expires ON itg_domains(expires_at);
|
||||
|
||||
-- ─── Expirations ──────────────────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS itg_expirations (
|
||||
id BIGINT PRIMARY KEY,
|
||||
organization_id BIGINT NOT NULL,
|
||||
organization_name TEXT,
|
||||
resource_id BIGINT,
|
||||
resource_type TEXT,
|
||||
resource_name TEXT,
|
||||
expiration_type TEXT,
|
||||
description TEXT,
|
||||
expiration_date TIMESTAMPTZ,
|
||||
notify BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ,
|
||||
updated_at TIMESTAMPTZ,
|
||||
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_itg_expirations_org ON itg_expirations(organization_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_itg_expirations_date ON itg_expirations(expiration_date);
|
||||
|
||||
-- ─── Sync History ─────────────────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS itg_sync_history (
|
||||
id SERIAL PRIMARY KEY,
|
||||
sync_type TEXT NOT NULL DEFAULT 'full',
|
||||
status TEXT NOT NULL,
|
||||
triggered_by TEXT DEFAULT 'system',
|
||||
started_at TIMESTAMPTZ NOT NULL,
|
||||
completed_at TIMESTAMPTZ,
|
||||
duration_ms INTEGER,
|
||||
entities JSONB DEFAULT '[]',
|
||||
error TEXT,
|
||||
total_upserted INTEGER DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_itg_sync_history_started ON itg_sync_history(started_at DESC);
|
||||
23
migrations/039_create_veeam_rpo_tickets_table.sql
Normal file
23
migrations/039_create_veeam_rpo_tickets_table.sql
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
-- Migration 039: Create veeam_rpo_tickets tracking table
|
||||
-- Tracks one open Autotask ticket per Veeam workstation job for RPO-based alerting
|
||||
|
||||
CREATE TABLE IF NOT EXISTS veeam_rpo_tickets (
|
||||
id SERIAL PRIMARY KEY,
|
||||
job_instance_uid TEXT NOT NULL UNIQUE,
|
||||
job_name TEXT NOT NULL,
|
||||
org_name TEXT NOT NULL,
|
||||
at_ticket_id BIGINT,
|
||||
at_ticket_number TEXT,
|
||||
priority_level TEXT NOT NULL DEFAULT 'medium' CHECK (priority_level IN ('medium', 'high', 'critical')),
|
||||
hours_overdue NUMERIC(10,2),
|
||||
failure_category TEXT,
|
||||
opened_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
resolved_at TIMESTAMPTZ,
|
||||
last_checked_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_veeam_rpo_tickets_job ON veeam_rpo_tickets(job_instance_uid);
|
||||
CREATE INDEX IF NOT EXISTS idx_veeam_rpo_tickets_open ON veeam_rpo_tickets(resolved_at) WHERE resolved_at IS NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_veeam_rpo_tickets_org ON veeam_rpo_tickets(org_name);
|
||||
Loading…
Add table
Add a link
Reference in a new issue