1use crate::types::{AppError, Result};
2use serde::{Deserialize, Serialize};
3use sqlx::{PgPool, Row};
4use std::time::{SystemTime, UNIX_EPOCH};
5
6fn now_ts() -> i64 {
7 SystemTime::now()
8 .duration_since(UNIX_EPOCH)
9 .unwrap()
10 .as_secs() as i64
11}
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct Alert {
15 pub id: String,
16 pub severity: String,
17 pub source: String,
18 pub title: String,
19 pub message: String,
20 pub resolved: bool,
21 pub created_at: i64,
22 pub resolved_at: Option<i64>,
23 pub resolved_by: Option<String>,
24}
25
26pub async fn create_alert(
27 pool: &PgPool,
28 severity: &str,
29 source: &str,
30 title: &str,
31 message: &str,
32) -> Result<Alert> {
33 let id = uuid::Uuid::new_v4().to_string();
34 let now = now_ts();
35
36 sqlx::query(
37 "INSERT INTO alerts (id, severity, source, title, message, resolved, created_at)
38 VALUES ($1, $2, $3, $4, $5, FALSE, $6)",
39 )
40 .bind(&id)
41 .bind(severity)
42 .bind(source)
43 .bind(title)
44 .bind(message)
45 .bind(now)
46 .execute(pool)
47 .await
48 .map_err(|e| AppError::Database(e.to_string()))?;
49
50 Ok(Alert {
51 id,
52 severity: severity.to_string(),
53 source: source.to_string(),
54 title: title.to_string(),
55 message: message.to_string(),
56 resolved: false,
57 created_at: now,
58 resolved_at: None,
59 resolved_by: None,
60 })
61}
62
63pub async fn list_alerts(
64 pool: &PgPool,
65 severity_filter: Option<&str>,
66 resolved_filter: Option<bool>,
67 limit: i64,
68) -> Result<Vec<Alert>> {
69 let mut query = String::from(
71 "SELECT id, severity, source, title, message, resolved, created_at, resolved_at, resolved_by
72 FROM alerts WHERE 1=1"
73 );
74 let mut bind_idx = 1;
75
76 if severity_filter.is_some() {
77 query.push_str(&format!(" AND severity = ${}", bind_idx));
78 bind_idx += 1;
79 }
80 if resolved_filter.is_some() {
81 query.push_str(&format!(" AND resolved = ${}", bind_idx));
82 bind_idx += 1;
83 }
84 let _ = bind_idx;
85
86 query.push_str(&format!(" ORDER BY created_at DESC LIMIT {}", limit));
87
88 let rows = match (severity_filter, resolved_filter) {
90 (Some(sev), Some(res)) => {
91 sqlx::query(
92 "SELECT id, severity, source, title, message, resolved, created_at, resolved_at, resolved_by
93 FROM alerts WHERE severity = $1 AND resolved = $2
94 ORDER BY created_at DESC LIMIT $3"
95 )
96 .bind(sev)
97 .bind(res)
98 .bind(limit)
99 .fetch_all(pool)
100 .await
101 }
102 (Some(sev), None) => {
103 sqlx::query(
104 "SELECT id, severity, source, title, message, resolved, created_at, resolved_at, resolved_by
105 FROM alerts WHERE severity = $1
106 ORDER BY created_at DESC LIMIT $2"
107 )
108 .bind(sev)
109 .bind(limit)
110 .fetch_all(pool)
111 .await
112 }
113 (None, Some(res)) => {
114 sqlx::query(
115 "SELECT id, severity, source, title, message, resolved, created_at, resolved_at, resolved_by
116 FROM alerts WHERE resolved = $1
117 ORDER BY created_at DESC LIMIT $2"
118 )
119 .bind(res)
120 .bind(limit)
121 .fetch_all(pool)
122 .await
123 }
124 (None, None) => {
125 sqlx::query(
126 "SELECT id, severity, source, title, message, resolved, created_at, resolved_at, resolved_by
127 FROM alerts ORDER BY created_at DESC LIMIT $1"
128 )
129 .bind(limit)
130 .fetch_all(pool)
131 .await
132 }
133 }
134 .map_err(|e| AppError::Database(e.to_string()))?;
135
136 rows.iter()
137 .map(|row| {
138 Ok(Alert {
139 id: row.get("id"),
140 severity: row.get("severity"),
141 source: row.get("source"),
142 title: row.get("title"),
143 message: row.get("message"),
144 resolved: row.get("resolved"),
145 created_at: row.get("created_at"),
146 resolved_at: row.get("resolved_at"),
147 resolved_by: row.get("resolved_by"),
148 })
149 })
150 .collect()
151}
152
153pub async fn resolve_alert(pool: &PgPool, alert_id: &str, resolved_by: Option<&str>) -> Result<()> {
154 let now = now_ts();
155
156 let result = sqlx::query(
157 "UPDATE alerts SET resolved = TRUE, resolved_at = $1, resolved_by = $2 WHERE id = $3 AND resolved = FALSE"
158 )
159 .bind(now)
160 .bind(resolved_by)
161 .bind(alert_id)
162 .execute(pool)
163 .await
164 .map_err(|e| AppError::Database(e.to_string()))?;
165
166 if result.rows_affected() == 0 {
167 return Err(AppError::NotFound(
168 "Alert not found or already resolved".to_string(),
169 ));
170 }
171
172 Ok(())
173}
174
175pub async fn get_active_alert_count(pool: &PgPool) -> Result<i64> {
176 let row = sqlx::query("SELECT COUNT(*) as cnt FROM alerts WHERE resolved = FALSE")
177 .fetch_one(pool)
178 .await
179 .map_err(|e| AppError::Database(e.to_string()))?;
180
181 Ok(row.get("cnt"))
182}