-- V1__init.sql - Consolidated schema for Cameleer3 -- Extensions CREATE EXTENSION IF NOT EXISTS timescaledb; CREATE EXTENSION IF NOT EXISTS timescaledb_toolkit; -- ============================================================= -- RBAC -- ============================================================= CREATE TABLE users ( user_id TEXT PRIMARY KEY, provider TEXT NOT NULL, email TEXT, display_name TEXT, password_hash TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE roles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL UNIQUE, description TEXT NOT NULL DEFAULT '', scope TEXT NOT NULL DEFAULT 'custom', system BOOLEAN NOT NULL DEFAULT false, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); INSERT INTO roles (id, name, description, scope, system) VALUES ('00000000-0000-0000-0000-000000000001', 'AGENT', 'Agent registration and data ingestion', 'system-wide', true), ('00000000-0000-0000-0000-000000000002', 'VIEWER', 'Read-only access to dashboards and data', 'system-wide', true), ('00000000-0000-0000-0000-000000000003', 'OPERATOR', 'Operational commands (start/stop/configure agents)', 'system-wide', true), ('00000000-0000-0000-0000-000000000004', 'ADMIN', 'Full administrative access', 'system-wide', true); CREATE TABLE groups ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL UNIQUE, parent_group_id UUID REFERENCES groups(id) ON DELETE SET NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- Built-in Admins group INSERT INTO groups (id, name) VALUES ('00000000-0000-0000-0000-000000000010', 'Admins'); CREATE TABLE group_roles ( group_id UUID NOT NULL REFERENCES groups(id) ON DELETE CASCADE, role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE, PRIMARY KEY (group_id, role_id) ); -- Assign ADMIN role to Admins group INSERT INTO group_roles (group_id, role_id) VALUES ('00000000-0000-0000-0000-000000000010', '00000000-0000-0000-0000-000000000004'); CREATE TABLE user_groups ( user_id TEXT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, group_id UUID NOT NULL REFERENCES groups(id) ON DELETE CASCADE, PRIMARY KEY (user_id, group_id) ); CREATE TABLE user_roles ( user_id TEXT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE, PRIMARY KEY (user_id, role_id) ); CREATE INDEX idx_user_roles_user_id ON user_roles(user_id); CREATE INDEX idx_user_groups_user_id ON user_groups(user_id); CREATE INDEX idx_group_roles_group_id ON group_roles(group_id); CREATE INDEX idx_groups_parent ON groups(parent_group_id); -- ============================================================= -- Execution data (TimescaleDB hypertables) -- ============================================================= CREATE TABLE executions ( execution_id TEXT NOT NULL, route_id TEXT NOT NULL, agent_id TEXT NOT NULL, application_name TEXT NOT NULL, status TEXT NOT NULL, correlation_id TEXT, exchange_id TEXT, start_time TIMESTAMPTZ NOT NULL, end_time TIMESTAMPTZ, duration_ms BIGINT, error_message TEXT, error_stacktrace TEXT, diagram_content_hash TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (execution_id, start_time) ); SELECT create_hypertable('executions', 'start_time', chunk_time_interval => INTERVAL '1 day'); CREATE INDEX idx_executions_agent_time ON executions (agent_id, start_time DESC); CREATE INDEX idx_executions_route_time ON executions (route_id, start_time DESC); CREATE INDEX idx_executions_app_time ON executions (application_name, start_time DESC); CREATE INDEX idx_executions_correlation ON executions (correlation_id); CREATE TABLE processor_executions ( id BIGSERIAL, execution_id TEXT NOT NULL, processor_id TEXT NOT NULL, processor_type TEXT NOT NULL, diagram_node_id TEXT, application_name TEXT NOT NULL, route_id TEXT NOT NULL, depth INT NOT NULL, parent_processor_id TEXT, status TEXT NOT NULL, start_time TIMESTAMPTZ NOT NULL, end_time TIMESTAMPTZ, duration_ms BIGINT, error_message TEXT, error_stacktrace TEXT, input_body TEXT, output_body TEXT, input_headers JSONB, output_headers JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (execution_id, processor_id, start_time) ); SELECT create_hypertable('processor_executions', 'start_time', chunk_time_interval => INTERVAL '1 day'); CREATE INDEX idx_proc_exec_execution ON processor_executions (execution_id); CREATE INDEX idx_proc_exec_type_time ON processor_executions (processor_type, start_time DESC); -- ============================================================= -- Agent metrics -- ============================================================= CREATE TABLE agent_metrics ( agent_id TEXT NOT NULL, metric_name TEXT NOT NULL, metric_value DOUBLE PRECISION NOT NULL, tags JSONB, collected_at TIMESTAMPTZ NOT NULL, server_received_at TIMESTAMPTZ NOT NULL DEFAULT now() ); SELECT create_hypertable('agent_metrics', 'collected_at', chunk_time_interval => INTERVAL '1 day'); CREATE INDEX idx_metrics_agent_name ON agent_metrics (agent_id, metric_name, collected_at DESC); -- Retention: drop agent_metrics chunks older than 90 days SELECT add_retention_policy('agent_metrics', INTERVAL '90 days', if_not_exists => true); -- Compression: compress agent_metrics chunks older than 7 days ALTER TABLE agent_metrics SET (timescaledb.compress); SELECT add_compression_policy('agent_metrics', INTERVAL '7 days', if_not_exists => true); -- ============================================================= -- Route diagrams -- ============================================================= CREATE TABLE route_diagrams ( content_hash TEXT PRIMARY KEY, route_id TEXT NOT NULL, agent_id TEXT NOT NULL, definition TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_diagrams_route_agent ON route_diagrams (route_id, agent_id); -- ============================================================= -- Agent events -- ============================================================= CREATE TABLE agent_events ( id BIGSERIAL PRIMARY KEY, agent_id TEXT NOT NULL, app_id TEXT NOT NULL, event_type TEXT NOT NULL, detail TEXT, timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_agent_events_agent ON agent_events(agent_id, timestamp DESC); CREATE INDEX idx_agent_events_app ON agent_events(app_id, timestamp DESC); CREATE INDEX idx_agent_events_time ON agent_events(timestamp DESC); -- ============================================================= -- Server configuration -- ============================================================= CREATE TABLE server_config ( config_key TEXT PRIMARY KEY, config_val JSONB NOT NULL, updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_by TEXT ); -- ============================================================= -- Admin -- ============================================================= CREATE TABLE audit_log ( id BIGSERIAL PRIMARY KEY, timestamp TIMESTAMPTZ NOT NULL DEFAULT now(), username TEXT NOT NULL, action TEXT NOT NULL, category TEXT NOT NULL, target TEXT, detail JSONB, result TEXT NOT NULL, ip_address TEXT, user_agent TEXT ); CREATE INDEX idx_audit_log_timestamp ON audit_log (timestamp DESC); CREATE INDEX idx_audit_log_username ON audit_log (username); CREATE INDEX idx_audit_log_category ON audit_log (category); CREATE INDEX idx_audit_log_action ON audit_log (action); CREATE INDEX idx_audit_log_target ON audit_log (target); -- ============================================================= -- Continuous aggregates -- ============================================================= CREATE MATERIALIZED VIEW stats_1m_all WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS SELECT time_bucket('1 minute', start_time) AS bucket, COUNT(*) AS total_count, COUNT(*) FILTER (WHERE status = 'FAILED') AS failed_count, COUNT(*) FILTER (WHERE status = 'RUNNING') AS running_count, SUM(duration_ms) AS duration_sum, MAX(duration_ms) AS duration_max, approx_percentile(0.99, percentile_agg(duration_ms::DOUBLE PRECISION)) AS p99_duration FROM executions WHERE status IS NOT NULL GROUP BY bucket WITH NO DATA; SELECT add_continuous_aggregate_policy('stats_1m_all', start_offset => INTERVAL '1 hour', end_offset => INTERVAL '1 minute', schedule_interval => INTERVAL '1 minute'); CREATE MATERIALIZED VIEW stats_1m_app WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS SELECT time_bucket('1 minute', start_time) AS bucket, application_name, COUNT(*) AS total_count, COUNT(*) FILTER (WHERE status = 'FAILED') AS failed_count, COUNT(*) FILTER (WHERE status = 'RUNNING') AS running_count, SUM(duration_ms) AS duration_sum, MAX(duration_ms) AS duration_max, approx_percentile(0.99, percentile_agg(duration_ms::DOUBLE PRECISION)) AS p99_duration FROM executions WHERE status IS NOT NULL GROUP BY bucket, application_name WITH NO DATA; SELECT add_continuous_aggregate_policy('stats_1m_app', start_offset => INTERVAL '1 hour', end_offset => INTERVAL '1 minute', schedule_interval => INTERVAL '1 minute'); CREATE MATERIALIZED VIEW stats_1m_route WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS SELECT time_bucket('1 minute', start_time) AS bucket, application_name, route_id, COUNT(*) AS total_count, COUNT(*) FILTER (WHERE status = 'FAILED') AS failed_count, COUNT(*) FILTER (WHERE status = 'RUNNING') AS running_count, SUM(duration_ms) AS duration_sum, MAX(duration_ms) AS duration_max, approx_percentile(0.99, percentile_agg(duration_ms::DOUBLE PRECISION)) AS p99_duration FROM executions WHERE status IS NOT NULL GROUP BY bucket, application_name, route_id WITH NO DATA; SELECT add_continuous_aggregate_policy('stats_1m_route', start_offset => INTERVAL '1 hour', end_offset => INTERVAL '1 minute', schedule_interval => INTERVAL '1 minute'); CREATE MATERIALIZED VIEW stats_1m_processor WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS SELECT time_bucket('1 minute', start_time) AS bucket, application_name, route_id, processor_type, COUNT(*) AS total_count, COUNT(*) FILTER (WHERE status = 'FAILED') AS failed_count, SUM(duration_ms) AS duration_sum, MAX(duration_ms) AS duration_max, approx_percentile(0.99, percentile_agg(duration_ms::DOUBLE PRECISION)) AS p99_duration FROM processor_executions GROUP BY bucket, application_name, route_id, processor_type WITH NO DATA; SELECT add_continuous_aggregate_policy('stats_1m_processor', start_offset => INTERVAL '1 hour', end_offset => INTERVAL '1 minute', schedule_interval => INTERVAL '1 minute'); CREATE MATERIALIZED VIEW stats_1m_processor_detail WITH (timescaledb.continuous) AS SELECT time_bucket('1 minute', start_time) AS bucket, application_name, route_id, processor_id, processor_type, COUNT(*) AS total_count, COUNT(*) FILTER (WHERE status = 'FAILED') AS failed_count, SUM(duration_ms) AS duration_sum, MAX(duration_ms) AS duration_max, approx_percentile(0.99, percentile_agg(duration_ms)) AS p99_duration FROM processor_executions GROUP BY bucket, application_name, route_id, processor_id, processor_type; SELECT add_continuous_aggregate_policy('stats_1m_processor_detail', start_offset => INTERVAL '1 hour', end_offset => INTERVAL '1 minute', schedule_interval => INTERVAL '1 minute');