Files
cloudlense/website-monitoring-frontend/databaseStructure.txt
T
Dennis 14a32bdc0d feat: initialize monorepo with full dev team best practices
- Unified monorepo with backend (Express), frontend (Next.js), and devops
- Backend: ESLint, Prettier, Jest tests (3 passing), health endpoint, .env.example
- Frontend: Fixed build errors, fixed all lint errors (0 remaining), tests passing
- DevOps: Docker Compose with PostgreSQL, backend, frontend + healthchecks
- CI/CD: 3 GitHub Actions workflows (backend, frontend, docker integration)
- DX: Husky pre-commit hooks with smart change detection
- Docs: Root README with architecture, CONTRIBUTING.md, PR template

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-06 00:05:50 +01:00

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;