brows3r_lib/notifications/
mod.rs1use std::sync::Arc;
18
19use serde::{Deserialize, Serialize};
20use tokio::sync::RwLock;
21
22use crate::{
23 error::AppError,
24 events::{emit, EventEmitter, EventKind},
25};
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(rename_all = "camelCase")]
34pub enum Severity {
35 Info,
36 Warning,
37 Error,
38 Success,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(rename_all = "camelCase")]
49pub enum NotificationCategory {
50 UserInitiated,
52 Background,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
65#[serde(rename_all = "camelCase")]
66pub struct Notification {
67 pub id: String,
69 pub severity: Severity,
70 pub category: NotificationCategory,
71 pub title: String,
72 pub message: String,
73 pub resource: Option<String>,
75 pub operation: Option<String>,
77 pub timestamp: i64,
79 pub details: Option<serde_json::Value>,
81}
82
83pub struct NotificationLog {
95 capacity: usize,
96 entries: std::collections::VecDeque<Notification>,
98}
99
100impl NotificationLog {
101 pub fn new() -> Self {
103 Self::with_capacity(500)
104 }
105
106 pub fn with_capacity(capacity: usize) -> Self {
108 let cap = capacity.max(1);
109 Self {
110 capacity: cap,
111 entries: std::collections::VecDeque::with_capacity(cap),
112 }
113 }
114
115 pub fn push(&mut self, notification: Notification) {
118 if self.entries.len() == self.capacity {
119 self.entries.pop_front();
120 }
121 self.entries.push_back(notification);
122 }
123
124 pub fn push_with_broadcast<E: EventEmitter>(
128 &mut self,
129 notification: Notification,
130 channel: &E,
131 ) -> Result<(), AppError> {
132 let notif_clone = notification.clone();
133 self.push(notification);
134 emit(channel, EventKind::NotificationNew, ¬if_clone)
135 }
136
137 pub fn list(&self, since: Option<i64>) -> Vec<Notification> {
140 match since {
141 None => self.entries.iter().cloned().collect(),
142 Some(since_ms) => self
143 .entries
144 .iter()
145 .filter(|n| n.timestamp >= since_ms)
146 .cloned()
147 .collect(),
148 }
149 }
150
151 pub fn dismiss(&mut self, id: &str) -> bool {
154 if let Some(pos) = self.entries.iter().position(|n| n.id == id) {
155 self.entries.remove(pos);
156 true
157 } else {
158 false
159 }
160 }
161
162 pub fn clear_all(&mut self) {
164 self.entries.clear();
165 }
166}
167
168impl Default for NotificationLog {
169 fn default() -> Self {
170 Self::new()
171 }
172}
173
174#[derive(Clone)]
183pub struct NotificationLogHandle(pub Arc<RwLock<NotificationLog>>);
184
185impl NotificationLogHandle {
186 pub fn new(log: NotificationLog) -> Self {
187 Self(Arc::new(RwLock::new(log)))
188 }
189}
190
191impl Default for NotificationLogHandle {
192 fn default() -> Self {
193 Self::new(NotificationLog::new())
194 }
195}
196
197pub mod os;
202
203#[cfg(test)]
208mod tests {
209 use super::*;
210 use crate::events::MockChannel;
211
212 fn make_notif(id: &str, ts: i64) -> Notification {
213 Notification {
214 id: id.to_string(),
215 severity: Severity::Info,
216 category: NotificationCategory::Background,
217 title: "Test".to_string(),
218 message: "msg".to_string(),
219 resource: None,
220 operation: None,
221 timestamp: ts,
222 details: None,
223 }
224 }
225
226 #[test]
229 fn push_501_evicts_first() {
230 let mut log = NotificationLog::with_capacity(500);
231 for i in 0..501u32 {
232 log.push(make_notif(&i.to_string(), i as i64));
233 }
234 let entries = log.list(None);
235 assert_eq!(entries.len(), 500, "should hold exactly 500 entries");
236 assert_eq!(
238 entries[0].id, "1",
239 "oldest entry after eviction should be id=1"
240 );
241 assert_eq!(entries[499].id, "500", "newest entry should be id=500");
242 }
243
244 #[test]
245 fn push_then_dismiss_omits_from_list() {
246 let mut log = NotificationLog::new();
247 log.push(make_notif("a", 1000));
248 log.push(make_notif("b", 2000));
249 let removed = log.dismiss("a");
250 assert!(removed, "dismiss should return true");
251 let entries = log.list(None);
252 assert_eq!(entries.len(), 1);
253 assert_eq!(entries[0].id, "b");
254 }
255
256 #[test]
257 fn dismiss_missing_returns_false() {
258 let mut log = NotificationLog::new();
259 assert!(!log.dismiss("no-such-id"));
260 }
261
262 #[test]
263 fn list_with_since_filter() {
264 let mut log = NotificationLog::new();
265 log.push(make_notif("old", 1000));
266 log.push(make_notif("mid", 5000));
267 log.push(make_notif("new", 9000));
268
269 let after = log.list(Some(5000));
270 assert_eq!(after.len(), 2, "should include mid and new (>=5000)");
271 assert_eq!(after[0].id, "mid");
272 assert_eq!(after[1].id, "new");
273 }
274
275 #[test]
276 fn clear_all_empties_log() {
277 let mut log = NotificationLog::new();
278 log.push(make_notif("x", 0));
279 log.clear_all();
280 assert!(log.list(None).is_empty());
281 }
282
283 #[test]
286 fn push_with_broadcast_emits_notification_new() {
287 let mut log = NotificationLog::new();
288 let channel = MockChannel::default();
289 let notif = make_notif("broadcast-id", 12345);
290
291 log.push_with_broadcast(notif, &channel)
292 .expect("broadcast should succeed");
293
294 let emitted = channel.emitted();
295 assert_eq!(emitted.len(), 1);
296 assert_eq!(emitted[0].0, EventKind::NotificationNew);
297 let payload: Notification =
298 serde_json::from_value(emitted[0].1.clone()).expect("payload should be Notification");
299 assert_eq!(payload.id, "broadcast-id");
300 }
301}