From 941bde4f7c96ec43c9dc9123dc9d923ad39e0699 Mon Sep 17 00:00:00 2001 From: thibaud-lclr Date: Fri, 17 Apr 2026 14:36:57 +0200 Subject: [PATCH] feat(db): add graylog schema and processed_tickets multi-source migration --- .../migrations/009_graylog_auto_resolve.sql | 127 ++++++++++++++++++ src-tauri/src/db.rs | 10 +- 2 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 src-tauri/migrations/009_graylog_auto_resolve.sql diff --git a/src-tauri/migrations/009_graylog_auto_resolve.sql b/src-tauri/migrations/009_graylog_auto_resolve.sql new file mode 100644 index 0000000..5fe155f --- /dev/null +++ b/src-tauri/migrations/009_graylog_auto_resolve.sql @@ -0,0 +1,127 @@ +BEGIN; + +PRAGMA foreign_keys = OFF; + +DROP INDEX IF EXISTS idx_processed_tickets_tracker_artifact_unique; + +CREATE TABLE processed_tickets_new ( + id TEXT PRIMARY KEY, + tracker_id TEXT REFERENCES watched_trackers(id) ON DELETE CASCADE, + project_id TEXT REFERENCES projects(id) ON DELETE CASCADE, + source TEXT NOT NULL DEFAULT 'tuleap', + source_ref TEXT, + artifact_id INTEGER NOT NULL, + artifact_title TEXT NOT NULL, + artifact_data TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'Pending', + analyst_report TEXT, + developer_report TEXT, + worktree_path TEXT, + branch_name TEXT, + detected_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + processed_at TEXT +); + +INSERT INTO processed_tickets_new ( + id, + tracker_id, + project_id, + source, + source_ref, + artifact_id, + artifact_title, + artifact_data, + status, + analyst_report, + developer_report, + worktree_path, + branch_name, + detected_at, + processed_at +) +SELECT + pt.id, + pt.tracker_id, + wt.project_id, + 'tuleap', + NULL, + pt.artifact_id, + pt.artifact_title, + pt.artifact_data, + pt.status, + pt.analyst_report, + pt.developer_report, + pt.worktree_path, + pt.branch_name, + pt.detected_at, + pt.processed_at +FROM processed_tickets pt +LEFT JOIN watched_trackers wt ON wt.id = pt.tracker_id; + +DROP TABLE processed_tickets; +ALTER TABLE processed_tickets_new RENAME TO processed_tickets; + +CREATE UNIQUE INDEX idx_processed_tickets_tracker_artifact_unique +ON processed_tickets(tracker_id, artifact_id) +WHERE tracker_id IS NOT NULL; + +CREATE INDEX idx_processed_tickets_project_detected +ON processed_tickets(project_id, detected_at DESC); + +CREATE INDEX idx_processed_tickets_source_ref +ON processed_tickets(source, source_ref); + +CREATE TABLE graylog_credentials ( + id TEXT PRIMARY KEY, + project_id TEXT NOT NULL UNIQUE REFERENCES projects(id) ON DELETE CASCADE, + base_url TEXT NOT NULL, + api_token_encrypted TEXT NOT NULL, + analyst_agent_id TEXT NOT NULL REFERENCES agents(id), + developer_agent_id TEXT NOT NULL REFERENCES agents(id), + stream_id TEXT, + query_filter TEXT NOT NULL DEFAULT '', + polling_interval_minutes INTEGER NOT NULL DEFAULT 10, + lookback_minutes INTEGER NOT NULL DEFAULT 30, + score_threshold INTEGER NOT NULL DEFAULT 70, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) +); + +CREATE TABLE graylog_subjects ( + id TEXT PRIMARY KEY, + project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, + subject_key TEXT NOT NULL, + source TEXT NOT NULL, + normalized_message TEXT NOT NULL, + first_seen_at TEXT NOT NULL, + last_seen_at TEXT NOT NULL, + last_score INTEGER NOT NULL DEFAULT 0, + active_ticket_id TEXT REFERENCES processed_tickets(id), + status TEXT NOT NULL DEFAULT 'idle', + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + UNIQUE(project_id, subject_key) +); + +CREATE TABLE graylog_detections ( + id TEXT PRIMARY KEY, + subject_id TEXT NOT NULL REFERENCES graylog_subjects(id) ON DELETE CASCADE, + window_start TEXT NOT NULL, + window_end TEXT NOT NULL, + critical_count INTEGER NOT NULL DEFAULT 0, + error_count INTEGER NOT NULL DEFAULT 0, + warning_count INTEGER NOT NULL DEFAULT 0, + total_count INTEGER NOT NULL DEFAULT 0, + last_seen_at TEXT NOT NULL, + score INTEGER NOT NULL, + triggered INTEGER NOT NULL DEFAULT 0, + triggered_ticket_id TEXT REFERENCES processed_tickets(id), + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) +); + +CREATE INDEX idx_graylog_detections_subject_created +ON graylog_detections(subject_id, created_at DESC); + +PRAGMA foreign_keys = ON; + +COMMIT; diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index 0b330f2..8158f47 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -9,6 +9,7 @@ const MIGRATION_005: &str = include_str!("../migrations/005_orchestration_module const MIGRATION_006: &str = include_str!("../migrations/006_processed_tickets_unique_index.sql"); const MIGRATION_007: &str = include_str!("../migrations/007_normalize_timestamps_rfc3339.sql"); const MIGRATION_008: &str = include_str!("../migrations/008_project_scoped_tuleap_credentials.sql"); +const MIGRATION_009: &str = include_str!("../migrations/009_graylog_auto_resolve.sql"); pub fn init(db_path: &Path) -> Result { let conn = Connection::open(db_path)?; @@ -66,6 +67,10 @@ fn migrate(conn: &Connection) -> Result<()> { conn.execute_batch(MIGRATION_008)?; conn.pragma_update(None, "user_version", 8)?; } + if version < 9 { + conn.execute_batch(MIGRATION_009)?; + conn.pragma_update(None, "user_version", 9)?; + } Ok(()) } @@ -91,6 +96,9 @@ mod tests { tables, vec![ "agents", + "graylog_credentials", + "graylog_detections", + "graylog_subjects", "notifications", "processed_tickets", "project_agent_tasks", @@ -121,7 +129,7 @@ mod tests { let version: i32 = conn .pragma_query_value(None, "user_version", |row| row.get(0)) .unwrap(); - assert_eq!(version, 8); + assert_eq!(version, 9); } #[test]