50e25e3ee8
Rename subdirectories for a cleaner single-repo layout: - website-monitoring-backend/ → backend/ - website-monitoring-frontend/ → frontend/ - website-monitoring-devops/ → devops/ Update all references in package.json scripts, CI workflows, docker-compose, pre-commit hooks, and documentation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
995 lines
31 KiB
Plaintext
995 lines
31 KiB
Plaintext
-- Enable necessary extensions
|
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
|
CREATE EXTENSION IF NOT EXISTS "pg_stat_statements";
|
|
CREATE EXTENSION IF NOT EXISTS "pg_trgm";
|
|
|
|
------ ENUMS ------
|
|
-- Core enums for status and types
|
|
CREATE TYPE scan_status AS ENUM (
|
|
'pending',
|
|
'queued',
|
|
'running',
|
|
'completed',
|
|
'failed',
|
|
'cancelled'
|
|
);
|
|
|
|
CREATE TYPE severity_level AS ENUM (
|
|
'critical',
|
|
'high',
|
|
'medium',
|
|
'low',
|
|
'info'
|
|
);
|
|
|
|
CREATE TYPE comparison_operator AS ENUM (
|
|
'less_than',
|
|
'less_than_equal',
|
|
'greater_than',
|
|
'greater_than_equal',
|
|
'equal_to',
|
|
'not_equal_to'
|
|
);
|
|
|
|
CREATE TYPE metric_category AS ENUM (
|
|
'performance',
|
|
'seo',
|
|
'accessibility',
|
|
'best_practices',
|
|
'security',
|
|
'pwa'
|
|
);
|
|
|
|
CREATE TYPE resource_type AS ENUM (
|
|
'script',
|
|
'stylesheet',
|
|
'image',
|
|
'font',
|
|
'document',
|
|
'media',
|
|
'other'
|
|
);
|
|
|
|
CREATE TYPE notification_channel AS ENUM (
|
|
'email',
|
|
'slack',
|
|
'webhook',
|
|
'in_app'
|
|
);
|
|
|
|
CREATE TYPE subscription_tier AS ENUM (
|
|
'free',
|
|
'starter',
|
|
'professional',
|
|
'enterprise'
|
|
);
|
|
|
|
CREATE TYPE user_role AS ENUM (
|
|
'owner',
|
|
'admin',
|
|
'editor',
|
|
'viewer'
|
|
);
|
|
|
|
------ CORE TABLES ------
|
|
-- Organizations table
|
|
CREATE TABLE organizations (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
name VARCHAR NOT NULL,
|
|
subscription_tier subscription_tier DEFAULT 'free',
|
|
subscription_status VARCHAR DEFAULT 'active',
|
|
billing_email VARCHAR,
|
|
max_websites INTEGER DEFAULT 5,
|
|
max_users INTEGER DEFAULT 3,
|
|
scan_frequency_minutes INTEGER DEFAULT 60,
|
|
settings JSONB DEFAULT '{
|
|
"alert_email_digest": "daily",
|
|
"default_scan_depth": 3,
|
|
"retention_days": 90,
|
|
"enable_competitor_analysis": false
|
|
}'::jsonb,
|
|
metadata JSONB DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- Users table
|
|
CREATE TABLE users (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
email VARCHAR UNIQUE NOT NULL,
|
|
name VARCHAR,
|
|
organization_id UUID REFERENCES organizations(id),
|
|
role user_role DEFAULT 'viewer',
|
|
is_active BOOLEAN DEFAULT true,
|
|
last_login_at TIMESTAMPTZ,
|
|
settings JSONB DEFAULT '{
|
|
"email_notifications": true,
|
|
"notification_frequency": "instant",
|
|
"dashboard_layout": "default"
|
|
}'::jsonb,
|
|
metadata JSONB DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- Websites table
|
|
CREATE TABLE websites (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
organization_id UUID REFERENCES organizations(id) NOT NULL,
|
|
base_url VARCHAR NOT NULL,
|
|
name VARCHAR NOT NULL,
|
|
is_active BOOLEAN DEFAULT true,
|
|
crawl_settings JSONB DEFAULT '{
|
|
"max_pages": 100,
|
|
"max_depth": 3,
|
|
"exclude_patterns": [
|
|
"/admin/*",
|
|
"/api/*",
|
|
"*.pdf",
|
|
"*.jpg",
|
|
"*.png"
|
|
],
|
|
"include_patterns": ["/*"],
|
|
"respect_robots_txt": true,
|
|
"crawl_frequency": "daily",
|
|
"crawl_timing": "off_peak"
|
|
}'::jsonb,
|
|
scan_schedule JSONB DEFAULT '{
|
|
"frequency": "hourly",
|
|
"time_windows": ["0-6", "20-23"],
|
|
"days": ["monday", "tuesday", "wednesday", "thursday", "friday"]
|
|
}'::jsonb,
|
|
performance_budgets JSONB DEFAULT '{
|
|
"page_weight_kb": 1000,
|
|
"max_requests": 100,
|
|
"time_to_interactive_ms": 3000,
|
|
"first_contentful_paint_ms": 1000
|
|
}'::jsonb,
|
|
notifications JSONB DEFAULT '{
|
|
"channels": ["email"],
|
|
"thresholds": {
|
|
"performance": 90,
|
|
"accessibility": 90,
|
|
"seo": 90,
|
|
"best_practices": 90
|
|
}
|
|
}'::jsonb,
|
|
last_crawl_at TIMESTAMPTZ,
|
|
last_scan_at TIMESTAMPTZ,
|
|
metadata JSONB DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
UNIQUE(organization_id, base_url)
|
|
);
|
|
|
|
-- Competitor tracking
|
|
CREATE TABLE competitor_websites (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
website_id UUID REFERENCES websites(id) NOT NULL,
|
|
competitor_url VARCHAR NOT NULL,
|
|
name VARCHAR NOT NULL,
|
|
is_active BOOLEAN DEFAULT true,
|
|
scan_frequency VARCHAR DEFAULT 'daily',
|
|
metrics_to_track VARCHAR[] DEFAULT ARRAY[
|
|
'performance',
|
|
'seo',
|
|
'accessibility',
|
|
'best_practices'
|
|
],
|
|
last_scan_at TIMESTAMPTZ,
|
|
metadata JSONB DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
UNIQUE(website_id, competitor_url)
|
|
);
|
|
|
|
-- Pages table
|
|
CREATE TABLE pages (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
website_id UUID REFERENCES websites(id) NOT NULL,
|
|
url VARCHAR NOT NULL,
|
|
path VARCHAR NOT NULL,
|
|
title VARCHAR,
|
|
description TEXT,
|
|
content_hash VARCHAR,
|
|
content_type VARCHAR,
|
|
status_code INTEGER,
|
|
is_active BOOLEAN DEFAULT true,
|
|
priority INTEGER DEFAULT 1,
|
|
depth INTEGER DEFAULT 0,
|
|
parent_page_id UUID REFERENCES pages(id),
|
|
discovery_method VARCHAR DEFAULT 'crawl',
|
|
last_seen_at TIMESTAMPTZ,
|
|
metadata JSONB DEFAULT '{
|
|
"inbound_links": 0,
|
|
"outbound_links": 0,
|
|
"word_count": 0,
|
|
"has_canonical": false,
|
|
"is_indexable": true
|
|
}'::jsonb,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
UNIQUE(website_id, url)
|
|
);
|
|
|
|
------ METRIC DEFINITIONS AND THRESHOLDS ------
|
|
CREATE TABLE metric_definitions (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
key VARCHAR NOT NULL UNIQUE,
|
|
name VARCHAR NOT NULL,
|
|
description TEXT NOT NULL,
|
|
category metric_category NOT NULL,
|
|
unit VARCHAR,
|
|
is_core_metric BOOLEAN DEFAULT false,
|
|
default_threshold NUMERIC,
|
|
warning_threshold NUMERIC,
|
|
critical_threshold NUMERIC,
|
|
direction VARCHAR NOT NULL DEFAULT 'higher_is_better',
|
|
weight NUMERIC DEFAULT 1.0,
|
|
documentation_url TEXT,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- Populate core metrics
|
|
INSERT INTO metric_definitions
|
|
(key, name, description, category, unit, is_core_metric, default_threshold, warning_threshold, critical_threshold, direction)
|
|
VALUES
|
|
-- Core Web Vitals
|
|
('performance', 'Performance Score', 'Overall performance score of the website', 'performance', '%', true, 90, 80, 70, 'higher_is_better'),
|
|
('accessibility', 'Accessibility Score', 'Overall accessibility score of the website', 'accessibility', '%', true, 90, 80, 70, 'higher_is_better'),
|
|
('seo', 'SEO Score', 'Overall SEO score of the website', 'seo', '%', true, 90, 80, 70, 'higher_is_better'),
|
|
('bestPractices', 'Best Practices Score', 'Overall best practices score', 'best_practices', '%', true, 90, 80, 70, 'higher_is_better'),
|
|
|
|
-- Performance Metrics
|
|
('firstContentfulPaint', 'First Contentful Paint', 'Time when the first text or image is painted', 'performance', 'ms', true, 1800, 2500, 4000, 'lower_is_better'),
|
|
('largestContentfulPaint', 'Largest Contentful Paint', 'Time when the largest text or image is painted', 'performance', 'ms', true, 2500, 4000, 6000, 'lower_is_better'),
|
|
('totalBlockingTime', 'Total Blocking Time', 'Sum of all time periods between FCP and Time to Interactive', 'performance', 'ms', true, 200, 400, 600, 'lower_is_better'),
|
|
('cumulativeLayoutShift', 'Cumulative Layout Shift', 'Measures visual stability', 'performance', 'score', true, 0.1, 0.25, 0.4, 'lower_is_better'),
|
|
('speedIndex', 'Speed Index', 'How quickly content is visually displayed', 'performance', 'ms', true, 3400, 5800, 8800, 'lower_is_better'),
|
|
('interactive', 'Time to Interactive', 'Time to fully interactive', 'performance', 'ms', true, 3800, 7300, 12700, 'lower_is_better'),
|
|
|
|
-- Resource Metrics
|
|
('totalByteWeight', 'Total Byte Weight', 'Total size of all resources', 'performance', 'bytes', false, 1600000, 2400000, 3200000, 'lower_is_better'),
|
|
('serverResponseTime', 'Server Response Time', 'Time for server to respond to main document request', 'performance', 'ms', false, 100, 200, 400, 'lower_is_better'),
|
|
('networkRtt', 'Network Round Trip Time', 'Network round trip time', 'performance', 'ms', false, 40, 100, 150, 'lower_is_better'),
|
|
('networkServerLatency', 'Network Server Latency', 'Server latency in network requests', 'performance', 'ms', false, 30, 100, 150, 'lower_is_better');
|
|
|
|
------ SCANS AND RESULTS ------
|
|
-- Scans table
|
|
CREATE TABLE scans (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
website_id UUID REFERENCES websites(id) NOT NULL,
|
|
page_id UUID REFERENCES pages(id) NOT NULL,
|
|
triggered_by UUID REFERENCES users(id),
|
|
scan_type VARCHAR NOT NULL DEFAULT 'full',
|
|
status scan_status DEFAULT 'pending',
|
|
priority INTEGER DEFAULT 1,
|
|
categories metric_category[] DEFAULT ARRAY['performance', 'seo', 'accessibility', 'best_practices'],
|
|
device_type VARCHAR DEFAULT 'desktop',
|
|
user_agent VARCHAR,
|
|
lighthouse_version VARCHAR,
|
|
chrome_version VARCHAR,
|
|
environment JSONB DEFAULT '{}'::jsonb,
|
|
started_at TIMESTAMPTZ DEFAULT NOW(),
|
|
completed_at TIMESTAMPTZ,
|
|
duration_ms INTEGER,
|
|
error_message TEXT,
|
|
retry_count INTEGER DEFAULT 0,
|
|
metadata JSONB DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- Scan results
|
|
CREATE TABLE scan_results (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
scan_id UUID REFERENCES scans(id) NOT NULL,
|
|
category metric_category NOT NULL,
|
|
score NUMERIC,
|
|
raw_data JSONB,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- Metric values
|
|
CREATE TABLE metric_values (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
scan_id UUID REFERENCES scans(id) NOT NULL,
|
|
metric_id UUID REFERENCES metric_definitions(id) NOT NULL,
|
|
value NUMERIC NOT NULL,
|
|
raw_value VARCHAR,
|
|
unit VARCHAR,
|
|
is_passing BOOLEAN,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- Resource analysis
|
|
CREATE TABLE resource_analysis (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
scan_id UUID REFERENCES scans(id) NOT NULL,
|
|
resource_type resource_type NOT NULL,
|
|
url VARCHAR NOT NULL,
|
|
size_bytes INTEGER NOT NULL,
|
|
transfer_size_bytes INTEGER,
|
|
duration_ms INTEGER,
|
|
is_third_party BOOLEAN DEFAULT false,
|
|
is_cached BOOLEAN,
|
|
compression_ratio NUMERIC,
|
|
mime_type VARCHAR,
|
|
protocol VARCHAR,
|
|
priority VARCHAR,
|
|
status_code INTEGER,
|
|
metadata JSONB DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
------ MONITORING AND ALERTS ------
|
|
-- Alert configurations
|
|
CREATE TABLE alert_configurations (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
website_id UUID REFERENCES websites(id) NOT NULL,
|
|
name VARCHAR NOT NULL,
|
|
description TEXT,
|
|
metric_id UUID REFERENCES metric_definitions(id) NOT NULL,
|
|
threshold NUMERIC NOT NULL,
|
|
comparison comparison_operator DEFAULT 'less_than',
|
|
severity severity_level DEFAULT 'medium',
|
|
consecutive_count INTEGER DEFAULT 1,
|
|
cooldown_minutes INTEGER DEFAULT 60,
|
|
notification_channels notification_channel[] DEFAULT ARRAY['email'],
|
|
notification_template TEXT,
|
|
is_active BOOLEAN DEFAULT true,
|
|
last_triggered_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- Alerts
|
|
CREATE TABLE alerts (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
website_id UUID REFERENCES websites(id) NOT NULL,
|
|
page_id UUID REFERENCES pages(id),
|
|
config_id UUID REFERENCES alert_configurations(id),
|
|
metric_id UUID REFERENCES metric_definitions(id),
|
|
severity severity_level DEFAULT 'medium',
|
|
title VARCHAR NOT NULL,
|
|
message TEXT NOT NULL,
|
|
details JSONB DEFAULT '{}'::jsonb,
|
|
status VARCHAR DEFAULT 'open',
|
|
acknowledged_by UUID REFERENCES users(id),
|
|
acknowledged_at TIMESTAMPTZ,
|
|
resolved_at TIMESTAMPTZ,
|
|
resolution_note TEXT,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- Alert history
|
|
CREATE TABLE alert_history (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
alert_id UUID REFERENCES alerts(id) NOT NULL,
|
|
event_type VARCHAR NOT NULL,
|
|
event_data JSONB NOT NULL,
|
|
created_by UUID REFERENCES users(id),
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- Notification delivery
|
|
CREATE TABLE notification_deliveries (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
alert_id UUID REFERENCES alerts(id) NOT NULL,
|
|
channel notification_channel NOT NULL,
|
|
recipient VARCHAR NOT NULL,
|
|
content TEXT NOT NULL,
|
|
status VARCHAR DEFAULT 'pending',
|
|
sent_at TIMESTAMPTZ,
|
|
error_message TEXT,
|
|
retry_count INTEGER DEFAULT 0,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
------ CRAWL MANAGEMENT ------
|
|
-- Crawl queue
|
|
CREATE TABLE crawl_queue (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
website_id UUID REFERENCES websites(id) NOT NULL,
|
|
url VARCHAR NOT NULL,
|
|
priority INTEGER DEFAULT 1,
|
|
status VARCHAR DEFAULT 'pending',
|
|
parent_url VARCHAR,
|
|
discovery_depth INTEGER DEFAULT 0,
|
|
attempts INTEGER DEFAULT 0,
|
|
last_attempt_at TIMESTAMPTZ,
|
|
next_attempt_at TIMESTAMPTZ,
|
|
error_message TEXT,
|
|
metadata JSONB DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- Crawl sessions
|
|
CREATE TABLE crawl_sessions (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
website_id UUID REFERENCES websites(id) NOT NULL,
|
|
status VARCHAR DEFAULT 'running',
|
|
pages_discovered INTEGER DEFAULT 0,
|
|
pages_processed INTEGER DEFAULT 0,
|
|
start_url VARCHAR NOT NULL,
|
|
max_depth INTEGER,
|
|
started_at TIMESTAMPTZ DEFAULT NOW(),
|
|
completed_at TIMESTAMPTZ,
|
|
error_message TEXT,
|
|
metadata JSONB DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- URL patterns
|
|
CREATE TABLE url_patterns (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
website_id UUID REFERENCES websites(id) NOT NULL,
|
|
pattern VARCHAR NOT NULL,
|
|
pattern_type VARCHAR NOT NULL, -- 'include' or 'exclude'
|
|
description TEXT,
|
|
is_regex BOOLEAN DEFAULT false,
|
|
is_active BOOLEAN DEFAULT true,
|
|
priority INTEGER DEFAULT 1,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
------ PERFORMANCE BUDGETS ------
|
|
CREATE TABLE performance_budgets (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
website_id UUID REFERENCES websites(id) NOT NULL,
|
|
name VARCHAR NOT NULL,
|
|
description TEXT,
|
|
metric_id UUID REFERENCES metric_definitions(id),
|
|
budget_type VARCHAR NOT NULL, -- 'size', 'timing', 'count'
|
|
threshold NUMERIC NOT NULL,
|
|
applies_to JSONB DEFAULT '{
|
|
"resource_types": ["all"],
|
|
"paths": ["/*"]
|
|
}'::jsonb,
|
|
is_active BOOLEAN DEFAULT true,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- Budget violations
|
|
CREATE TABLE budget_violations (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
budget_id UUID REFERENCES performance_budgets(id) NOT NULL,
|
|
scan_id UUID REFERENCES scans(id) NOT NULL,
|
|
actual_value NUMERIC NOT NULL,
|
|
threshold_value NUMERIC NOT NULL,
|
|
percentage_over NUMERIC,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
------ CUSTOM DASHBOARDS AND REPORTS ------
|
|
-- Dashboard definitions
|
|
CREATE TABLE dashboards (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
organization_id UUID REFERENCES organizations(id) NOT NULL,
|
|
name VARCHAR NOT NULL,
|
|
description TEXT,
|
|
layout JSONB DEFAULT '[]'::jsonb,
|
|
is_default BOOLEAN DEFAULT false,
|
|
created_by UUID REFERENCES users(id),
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- Dashboard widgets
|
|
CREATE TABLE dashboard_widgets (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
dashboard_id UUID REFERENCES dashboards(id) NOT NULL,
|
|
widget_type VARCHAR NOT NULL,
|
|
name VARCHAR NOT NULL,
|
|
config JSONB NOT NULL,
|
|
position JSONB NOT NULL,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- Report templates
|
|
CREATE TABLE report_templates (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
organization_id UUID REFERENCES organizations(id) NOT NULL,
|
|
name VARCHAR NOT NULL,
|
|
description TEXT,
|
|
template_type VARCHAR NOT NULL,
|
|
content JSONB NOT NULL,
|
|
schedule JSONB DEFAULT NULL,
|
|
is_active BOOLEAN DEFAULT true,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- Generated reports
|
|
CREATE TABLE generated_reports (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
template_id UUID REFERENCES report_templates(id) NOT NULL,
|
|
website_id UUID REFERENCES websites(id),
|
|
generated_by UUID REFERENCES users(id),
|
|
report_data JSONB NOT NULL,
|
|
format VARCHAR DEFAULT 'pdf',
|
|
file_url VARCHAR,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
------ API ACCESS AND RATE LIMITING ------
|
|
-- API keys
|
|
CREATE TABLE api_keys (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
organization_id UUID REFERENCES organizations(id) NOT NULL,
|
|
name VARCHAR NOT NULL,
|
|
key_hash VARCHAR NOT NULL,
|
|
scopes VARCHAR[] DEFAULT ARRAY['read'],
|
|
rate_limit_per_minute INTEGER DEFAULT 60,
|
|
is_active BOOLEAN DEFAULT true,
|
|
last_used_at TIMESTAMPTZ,
|
|
expires_at TIMESTAMPTZ,
|
|
created_by UUID REFERENCES users(id),
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- Rate limiting
|
|
CREATE TABLE rate_limits (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
key_type VARCHAR NOT NULL, -- 'api_key', 'ip_address'
|
|
key_value VARCHAR NOT NULL,
|
|
window_start TIMESTAMPTZ NOT NULL,
|
|
request_count INTEGER DEFAULT 1,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
------ AUDIT LOGGING ------
|
|
CREATE TABLE audit_logs (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
organization_id UUID REFERENCES organizations(id),
|
|
user_id UUID REFERENCES users(id),
|
|
action VARCHAR NOT NULL,
|
|
entity_type VARCHAR NOT NULL,
|
|
entity_id UUID,
|
|
changes JSONB,
|
|
ip_address VARCHAR,
|
|
user_agent VARCHAR,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
------ VIEWS ------
|
|
-- Performance trend view
|
|
CREATE VIEW performance_trends AS
|
|
SELECT
|
|
w.id AS website_id,
|
|
w.name AS website_name,
|
|
p.url AS page_url,
|
|
m.key AS metric_key,
|
|
mv.value AS metric_value,
|
|
s.created_at AS scan_date,
|
|
AVG(mv.value) OVER (
|
|
PARTITION BY w.id, p.id, m.id
|
|
ORDER BY s.created_at
|
|
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
|
|
) AS rolling_average
|
|
FROM websites w
|
|
JOIN pages p ON p.website_id = w.id
|
|
JOIN scans s ON s.page_id = p.id
|
|
JOIN metric_values mv ON mv.scan_id = s.id
|
|
JOIN metric_definitions m ON m.id = mv.metric_id
|
|
WHERE s.created_at >= NOW() - INTERVAL '30 days';
|
|
|
|
-- Resource usage summary view
|
|
CREATE VIEW resource_usage_summary AS
|
|
SELECT
|
|
w.id AS website_id,
|
|
w.name AS website_name,
|
|
ra.resource_type,
|
|
COUNT(*) AS resource_count,
|
|
AVG(ra.size_bytes) AS avg_size,
|
|
SUM(ra.size_bytes) AS total_size,
|
|
AVG(ra.duration_ms) AS avg_duration
|
|
FROM websites w
|
|
JOIN scans s ON s.website_id = w.id
|
|
JOIN resource_analysis ra ON ra.scan_id = s.id
|
|
WHERE s.created_at >= NOW() - INTERVAL '24 hours'
|
|
GROUP BY w.id, w.name, ra.resource_type;
|
|
|
|
------ FUNCTIONS ------
|
|
-- Calculate health score
|
|
CREATE OR REPLACE FUNCTION calculate_health_score(website_id UUID)
|
|
RETURNS NUMERIC AS $$
|
|
DECLARE
|
|
score NUMERIC;
|
|
BEGIN
|
|
SELECT
|
|
AVG(
|
|
CASE
|
|
WHEN m.direction = 'higher_is_better' THEN
|
|
LEAST(mv.value / NULLIF(m.default_threshold, 0) * 100, 100)
|
|
ELSE
|
|
LEAST(m.default_threshold / NULLIF(mv.value, 0) * 100, 100)
|
|
END
|
|
)
|
|
INTO score
|
|
FROM scans s
|
|
JOIN metric_values mv ON mv.scan_id = s.id
|
|
JOIN metric_definitions m ON m.id = mv.metric_id
|
|
WHERE s.website_id = calculate_health_score.website_id
|
|
AND s.created_at >= NOW() - INTERVAL '24 hours'
|
|
AND m.is_core_metric = true;
|
|
|
|
RETURN COALESCE(score, 0);
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Update scan status
|
|
CREATE OR REPLACE FUNCTION update_scan_status()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
IF NEW.status = 'completed' THEN
|
|
-- Update website's last scan timestamp
|
|
UPDATE websites
|
|
SET last_scan_at = NOW()
|
|
WHERE id = NEW.website_id;
|
|
|
|
-- Check for alerts
|
|
INSERT INTO alerts (website_id, page_id, severity, title, message)
|
|
SELECT
|
|
NEW.website_id,
|
|
NEW.page_id,
|
|
ac.severity,
|
|
'Metric threshold exceeded',
|
|
format('%s is %s threshold of %s', m.name, ac.comparison, ac.threshold)
|
|
FROM metric_values mv
|
|
JOIN metric_definitions m ON m.id = mv.metric_id
|
|
JOIN alert_configurations ac ON ac.metric_id = m.id
|
|
WHERE mv.scan_id = NEW.id
|
|
AND ac.website_id = NEW.website_id
|
|
AND (
|
|
CASE ac.comparison
|
|
WHEN 'less_than' THEN mv.value < ac.threshold
|
|
WHEN 'greater_than' THEN mv.value > ac.threshold
|
|
WHEN 'equal_to' THEN mv.value = ac.threshold
|
|
END
|
|
);
|
|
END IF;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Create trigger for scan status updates
|
|
CREATE TRIGGER scan_status_update
|
|
AFTER UPDATE OF status ON scans
|
|
FOR EACH ROW
|
|
WHEN (OLD.status IS DISTINCT FROM NEW.status)
|
|
EXECUTE FUNCTION update_scan_status();
|
|
|
|
------ INDEXES ------
|
|
-- Performance indexes
|
|
CREATE INDEX idx_scans_website_status ON scans(website_id, status);
|
|
CREATE INDEX idx_scans_created_at ON scans(created_at);
|
|
CREATE INDEX idx_metric_values_scan_metric ON metric_values(scan_id, metric_id);
|
|
CREATE INDEX idx_pages_website_active ON pages(website_id, is_active);
|
|
CREATE INDEX idx_crawl_queue_status_priority ON crawl_queue(status, priority);
|
|
CREATE INDEX idx_alerts_website_status ON alerts(website_id, status);
|
|
CREATE INDEX idx_resource_analysis_scan ON resource_analysis(scan_id);
|
|
CREATE INDEX idx_audit_logs_organization ON audit_logs(organization_id, created_at);
|
|
CREATE INDEX idx_metric_values_created_at ON metric_values(created_at);
|
|
CREATE INDEX idx_pages_url_trgm ON pages USING gin (url gin_trgm_ops);
|
|
|
|
------ SECURITY POLICIES ------
|
|
-- RLS Policies
|
|
ALTER TABLE organizations ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE websites ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE pages ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE scans ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE metric_values ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE alerts ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE dashboards ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE api_keys ENABLE ROW LEVEL SECURITY;
|
|
|
|
-- Organization access
|
|
CREATE POLICY "Users can view their organization"
|
|
ON organizations
|
|
FOR SELECT
|
|
USING (id IN (
|
|
SELECT organization_id FROM users WHERE id = auth.uid()
|
|
));
|
|
|
|
-- Website access
|
|
CREATE POLICY "Users can view their organization's websites"
|
|
ON websites
|
|
FOR SELECT
|
|
USING (organization_id IN (
|
|
SELECT organization_id FROM users WHERE id = auth.uid()
|
|
));
|
|
|
|
CREATE POLICY "Admins can manage their organization's websites"
|
|
ON websites
|
|
FOR ALL
|
|
USING (
|
|
organization_id IN (
|
|
SELECT organization_id
|
|
FROM users
|
|
WHERE id = auth.uid() AND role IN ('admin', 'owner')
|
|
)
|
|
);
|
|
|
|
------ DATA RETENTION ------
|
|
-- Create retention policy function
|
|
CREATE OR REPLACE FUNCTION apply_data_retention()
|
|
RETURNS void AS $$
|
|
DECLARE
|
|
org RECORD;
|
|
BEGIN
|
|
-- Loop through organizations
|
|
FOR org IN SELECT id, (settings->>'retention_days')::integer AS retention_days
|
|
FROM organizations
|
|
WHERE settings->>'retention_days' IS NOT NULL
|
|
LOOP
|
|
-- Delete old scan data
|
|
DELETE FROM metric_values
|
|
WHERE scan_id IN (
|
|
SELECT id FROM scans
|
|
WHERE website_id IN (
|
|
SELECT id FROM websites WHERE organization_id = org.id
|
|
)
|
|
AND created_at < NOW() - (org.retention_days || ' days')::interval
|
|
);
|
|
|
|
-- Delete old resource analysis
|
|
DELETE FROM resource_analysis
|
|
WHERE scan_id IN (
|
|
SELECT id FROM scans
|
|
WHERE website_id IN (
|
|
SELECT id FROM websites WHERE organization_id = org.id
|
|
)
|
|
AND created_at < NOW() - (org.retention_days || ' days')::interval
|
|
);
|
|
|
|
-- Delete old scans
|
|
DELETE FROM scans
|
|
WHERE website_id IN (
|
|
SELECT id FROM websites WHERE organization_id = org.id
|
|
)
|
|
AND created_at < NOW() - (org.retention_days || ' days')::interval;
|
|
|
|
-- Archive resolved alerts
|
|
UPDATE alerts
|
|
SET status = 'archived'
|
|
WHERE website_id IN (
|
|
SELECT id FROM websites WHERE organization_id = org.id
|
|
)
|
|
AND status = 'resolved'
|
|
AND resolved_at < NOW() - (org.retention_days || ' days')::interval;
|
|
END LOOP;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
------ ADDITIONAL FUNCTIONS ------
|
|
-- Calculate competitor comparison
|
|
CREATE OR REPLACE FUNCTION calculate_competitor_comparison(website_id UUID)
|
|
RETURNS TABLE (
|
|
metric_key VARCHAR,
|
|
your_score NUMERIC,
|
|
competitor_avg NUMERIC,
|
|
competitor_best NUMERIC,
|
|
percentile NUMERIC
|
|
) AS $$
|
|
BEGIN
|
|
RETURN QUERY
|
|
WITH your_metrics AS (
|
|
SELECT
|
|
m.key,
|
|
mv.value
|
|
FROM scans s
|
|
JOIN metric_values mv ON mv.scan_id = s.id
|
|
JOIN metric_definitions m ON m.id = mv.metric_id
|
|
WHERE s.website_id = calculate_competitor_comparison.website_id
|
|
AND s.created_at = (
|
|
SELECT MAX(created_at)
|
|
FROM scans
|
|
WHERE website_id = calculate_competitor_comparison.website_id
|
|
)
|
|
),
|
|
competitor_metrics AS (
|
|
SELECT
|
|
m.key,
|
|
mv.value,
|
|
PERCENT_RANK() OVER (PARTITION BY m.key ORDER BY mv.value) as percentile
|
|
FROM scans s
|
|
JOIN metric_values mv ON mv.scan_id = s.id
|
|
JOIN metric_definitions m ON m.id = mv.metric_id
|
|
WHERE s.website_id IN (
|
|
SELECT competitor_url_id
|
|
FROM competitor_websites
|
|
WHERE website_id = calculate_competitor_comparison.website_id
|
|
)
|
|
AND s.created_at >= NOW() - INTERVAL '30 days'
|
|
)
|
|
SELECT
|
|
ym.key,
|
|
ym.value as your_score,
|
|
AVG(cm.value) as competitor_avg,
|
|
MAX(cm.value) as competitor_best,
|
|
MAX(cm.percentile) * 100 as percentile
|
|
FROM your_metrics ym
|
|
LEFT JOIN competitor_metrics cm ON cm.key = ym.key
|
|
GROUP BY ym.key, ym.value;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Generate performance report
|
|
CREATE OR REPLACE FUNCTION generate_performance_report(website_id UUID, days INTEGER)
|
|
RETURNS JSONB AS $$
|
|
DECLARE
|
|
report JSONB;
|
|
BEGIN
|
|
SELECT jsonb_build_object(
|
|
'website_info', (
|
|
SELECT jsonb_build_object(
|
|
'name', name,
|
|
'url', base_url,
|
|
'report_period', jsonb_build_object(
|
|
'start', NOW() - (days || ' days')::interval,
|
|
'end', NOW()
|
|
)
|
|
)
|
|
FROM websites
|
|
WHERE id = website_id
|
|
),
|
|
'performance_summary', (
|
|
SELECT jsonb_build_object(
|
|
'average_performance_score', AVG(mv.value),
|
|
'best_performance_score', MAX(mv.value),
|
|
'worst_performance_score', MIN(mv.value),
|
|
'trend', jsonb_agg(
|
|
jsonb_build_object(
|
|
'date', DATE(s.created_at),
|
|
'score', mv.value
|
|
) ORDER BY s.created_at
|
|
)
|
|
)
|
|
FROM scans s
|
|
JOIN metric_values mv ON mv.scan_id = s.id
|
|
JOIN metric_definitions md ON md.id = mv.metric_id
|
|
WHERE s.website_id = generate_performance_report.website_id
|
|
AND md.key = 'performance'
|
|
AND s.created_at >= NOW() - (days || ' days')::interval
|
|
),
|
|
'core_metrics', (
|
|
SELECT jsonb_object_agg(
|
|
md.key,
|
|
jsonb_build_object(
|
|
'average', AVG(mv.value),
|
|
'best', MAX(mv.value),
|
|
'worst', MIN(mv.value)
|
|
)
|
|
)
|
|
FROM scans s
|
|
JOIN metric_values mv ON mv.scan_id = s.id
|
|
JOIN metric_definitions md ON md.id = mv.metric_id
|
|
WHERE s.website_id = generate_performance_report.website_id
|
|
AND md.is_core_metric = true
|
|
AND s.created_at >= NOW() - (days || ' days')::interval
|
|
),
|
|
'resource_summary', (
|
|
SELECT jsonb_object_agg(
|
|
resource_type,
|
|
jsonb_build_object(
|
|
'count', COUNT(*),
|
|
'total_size', SUM(size_bytes),
|
|
'average_duration', AVG(duration_ms)
|
|
)
|
|
)
|
|
FROM scans s
|
|
JOIN resource_analysis ra ON ra.scan_id = s.id
|
|
WHERE s.website_id = generate_performance_report.website_id
|
|
AND s.created_at >= NOW() - (days || ' days')::interval
|
|
),
|
|
'alerts', (
|
|
SELECT jsonb_agg(
|
|
jsonb_build_object(
|
|
'severity', severity,
|
|
'message', message,
|
|
'created_at', created_at
|
|
)
|
|
)
|
|
FROM alerts
|
|
WHERE website_id = generate_performance_report.website_id
|
|
AND created_at >= NOW() - (days || ' days')::interval
|
|
)
|
|
) INTO report;
|
|
|
|
RETURN report;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
------ NOTIFICATIONS ------
|
|
-- Create notification function
|
|
CREATE OR REPLACE FUNCTION process_alert_notifications()
|
|
RETURNS trigger AS $$
|
|
DECLARE
|
|
website_record RECORD;
|
|
user_record RECORD;
|
|
BEGIN
|
|
-- Get website details
|
|
SELECT * INTO website_record
|
|
FROM websites
|
|
WHERE id = NEW.website_id;
|
|
|
|
-- Insert notification for each user in the organization
|
|
FOR user_record IN
|
|
SELECT u.*
|
|
FROM users u
|
|
WHERE u.organization_id = website_record.organization_id
|
|
AND (u.settings->>'email_notifications')::boolean = true
|
|
LOOP
|
|
INSERT INTO notification_deliveries (
|
|
alert_id,
|
|
channel,
|
|
recipient,
|
|
content
|
|
) VALUES (
|
|
NEW.id,
|
|
'email',
|
|
user_record.email,
|
|
format(
|
|
'Alert for %s: %s. Severity: %s',
|
|
website_record.name,
|
|
NEW.message,
|
|
NEW.severity
|
|
)
|
|
);
|
|
END LOOP;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Create trigger for alert notifications
|
|
CREATE TRIGGER alert_notification_trigger
|
|
AFTER INSERT ON alerts
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION process_alert_notifications();
|
|
|
|
------ MAINTENANCE PROCEDURES ------
|
|
-- Create maintenance function
|
|
CREATE OR REPLACE FUNCTION perform_maintenance()
|
|
RETURNS void AS $$
|
|
BEGIN
|
|
-- Clean up old rate limit records
|
|
DELETE FROM rate_limits
|
|
WHERE window_start < NOW() - INTERVAL '1 day';
|
|
|
|
-- Archive old notifications
|
|
UPDATE notification_deliveries
|
|
SET status = 'archived'
|
|
WHERE created_at < NOW() - INTERVAL '30 days';
|
|
|
|
-- Clean up expired API keys
|
|
UPDATE api_keys
|
|
SET is_active = false
|
|
WHERE expires_at < NOW();
|
|
|
|
-- Update statistics
|
|
ANALYZE websites;
|
|
ANALYZE scans;
|
|
ANALYZE metric_values;
|
|
ANALYZE resource_analysis;
|
|
|
|
-- Vacuum analyze for better query planning
|
|
VACUUM ANALYZE websites;
|
|
VACUUM ANALYZE scans;
|
|
VACUUM ANALYZE metric_values;
|
|
VACUUM ANALYZE resource_analysis;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|