v0.12.6-hotfix
This commit is contained in:
@@ -29,6 +29,14 @@ impl Db {
|
||||
Ok(conn.last_insert_rowid())
|
||||
}
|
||||
|
||||
pub fn update_event_metadata(&self, id: i64, metadata: &Value) -> Result<bool> {
|
||||
let conn = self.lock()?;
|
||||
Ok(conn.execute(
|
||||
queries::UPDATE_EVENT_METADATA,
|
||||
params![serde_json::to_string(metadata)?, id],
|
||||
)? > 0)
|
||||
}
|
||||
|
||||
pub fn list_events(&self, limit: u32) -> Result<Vec<EventLog>> {
|
||||
let conn = self.lock()?;
|
||||
let mut stmt = conn.prepare(queries::LIST_EVENTS)?;
|
||||
|
||||
+11
-1
@@ -38,9 +38,19 @@ mod tests {
|
||||
let loaded = db.get_device(&device.id).unwrap().unwrap();
|
||||
assert_eq!(loaded.mac, device.mac);
|
||||
assert_eq!(db.list_devices().unwrap().len(), 1);
|
||||
db.log_event("info", "test", "ok", &serde_json::json!({"a":1}))
|
||||
let event_id = db
|
||||
.log_event("info", "test", "ok", &serde_json::json!({"a":1}))
|
||||
.unwrap();
|
||||
assert_eq!(db.list_events(10).unwrap().len(), 1);
|
||||
db.update_event_metadata(
|
||||
event_id,
|
||||
&serde_json::json!({"a":1,"notification":{"status":"silent"}}),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
db.list_events(10).unwrap()[0].metadata["notification"]["status"],
|
||||
"silent"
|
||||
);
|
||||
{
|
||||
let conn = db.lock().unwrap();
|
||||
conn.execute(
|
||||
|
||||
+133
-9
@@ -64,14 +64,28 @@ fn alert_type_enabled(cfg: &NotificationSettings, kind: &str) -> bool {
|
||||
types.other
|
||||
}
|
||||
|
||||
fn should_send(cfg: &NotificationSettings, level: &str, kind: &str) -> bool {
|
||||
if !cfg.enabled || !alert_type_enabled(cfg, kind) {
|
||||
return false;
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum DeliveryDecision {
|
||||
NotApplicable,
|
||||
Silent(&'static str),
|
||||
Send,
|
||||
}
|
||||
|
||||
fn delivery_decision(cfg: &NotificationSettings, level: &str, kind: &str) -> DeliveryDecision {
|
||||
let candidate = level == "error" || level == "warn" || important_kind(kind);
|
||||
if !candidate {
|
||||
return DeliveryDecision::NotApplicable;
|
||||
}
|
||||
if level == "error" || level == "warn" {
|
||||
return true;
|
||||
if !cfg.enabled {
|
||||
return DeliveryDecision::Silent("notifications_disabled");
|
||||
}
|
||||
cfg.mode == "important" && important_kind(kind)
|
||||
if !alert_type_enabled(cfg, kind) {
|
||||
return DeliveryDecision::Silent("alert_type_disabled");
|
||||
}
|
||||
if level != "error" && level != "warn" && cfg.mode != "important" {
|
||||
return DeliveryDecision::Silent("mode_filtered");
|
||||
}
|
||||
DeliveryDecision::Send
|
||||
}
|
||||
|
||||
fn cooldown_key(cfg: &NotificationSettings, kind: &str, metadata: &Value) -> String {
|
||||
@@ -100,15 +114,85 @@ fn take_cooldown(cfg: &NotificationSettings, kind: &str, metadata: &Value) -> bo
|
||||
true
|
||||
}
|
||||
|
||||
fn with_notification_status(
|
||||
metadata: &Value,
|
||||
status: &str,
|
||||
reason: Option<&str>,
|
||||
provider: &str,
|
||||
) -> Value {
|
||||
let mut updated = match metadata {
|
||||
Value::Object(map) => Value::Object(map.clone()),
|
||||
other => json!({"data": other}),
|
||||
};
|
||||
let mut notification = serde_json::Map::new();
|
||||
notification.insert("status".into(), Value::String(status.into()));
|
||||
if let Some(reason) = reason {
|
||||
notification.insert("reason".into(), Value::String(reason.into()));
|
||||
}
|
||||
if !provider.trim().is_empty() {
|
||||
notification.insert("provider".into(), Value::String(provider.into()));
|
||||
}
|
||||
if let Some(object) = updated.as_object_mut() {
|
||||
object.insert("notification".into(), Value::Object(notification));
|
||||
}
|
||||
updated
|
||||
}
|
||||
|
||||
fn persist_notification_status(
|
||||
state: &AppState,
|
||||
event_id: Option<i64>,
|
||||
metadata: &Value,
|
||||
status: &str,
|
||||
reason: Option<&str>,
|
||||
provider: &str,
|
||||
) {
|
||||
let Some(event_id) = event_id else {
|
||||
return;
|
||||
};
|
||||
let updated = with_notification_status(metadata, status, reason, provider);
|
||||
match state.db.update_event_metadata(event_id, &updated) {
|
||||
Ok(true) => state.broadcast(
|
||||
"log.updated",
|
||||
json!({"id": event_id, "metadata": updated}),
|
||||
),
|
||||
Ok(false) => tracing::warn!(event_id, "cannot update missing event log metadata"),
|
||||
Err(err) => tracing::warn!(event_id, error=?err, "cannot update event log metadata"),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn dispatch(
|
||||
state: AppState,
|
||||
event_id: Option<i64>,
|
||||
level: String,
|
||||
kind: String,
|
||||
message: String,
|
||||
metadata: Value,
|
||||
) {
|
||||
let cfg = state.settings.read().await.notifications.clone();
|
||||
if !should_send(&cfg, &level, &kind) || !take_cooldown(&cfg, &kind, &metadata) {
|
||||
match delivery_decision(&cfg, &level, &kind) {
|
||||
DeliveryDecision::NotApplicable => return,
|
||||
DeliveryDecision::Silent(reason) => {
|
||||
persist_notification_status(
|
||||
&state,
|
||||
event_id,
|
||||
&metadata,
|
||||
"silent",
|
||||
Some(reason),
|
||||
&cfg.provider,
|
||||
);
|
||||
return;
|
||||
}
|
||||
DeliveryDecision::Send => {}
|
||||
}
|
||||
if !take_cooldown(&cfg, &kind, &metadata) {
|
||||
persist_notification_status(
|
||||
&state,
|
||||
event_id,
|
||||
&metadata,
|
||||
"silent",
|
||||
Some("cooldown"),
|
||||
&cfg.provider,
|
||||
);
|
||||
return;
|
||||
}
|
||||
let title = format!(
|
||||
@@ -139,8 +223,26 @@ pub async fn dispatch(
|
||||
}
|
||||
_ => Err("unsupported notification provider".into()),
|
||||
};
|
||||
if let Err(err) = result {
|
||||
tracing::warn!(provider=%cfg.provider, error=%err, "notification delivery failed");
|
||||
match result {
|
||||
Ok(()) => persist_notification_status(
|
||||
&state,
|
||||
event_id,
|
||||
&metadata,
|
||||
"sent",
|
||||
None,
|
||||
&cfg.provider,
|
||||
),
|
||||
Err(err) => {
|
||||
persist_notification_status(
|
||||
&state,
|
||||
event_id,
|
||||
&metadata,
|
||||
"failed",
|
||||
Some("delivery_failed"),
|
||||
&cfg.provider,
|
||||
);
|
||||
tracing::warn!(provider=%cfg.provider, error=%err, "notification delivery failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,6 +263,28 @@ mod tests {
|
||||
assert!(alert_type_enabled(&cfg, "zone.sensor_discrepancy"));
|
||||
assert!(!alert_type_enabled(&cfg, "zone.action_error"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_alert_type_is_recorded_as_silent() {
|
||||
let mut cfg = NotificationSettings::default();
|
||||
cfg.enabled = true;
|
||||
cfg.mode = "important".into();
|
||||
cfg.alert_types.sensor_discrepancy = false;
|
||||
|
||||
assert_eq!(
|
||||
delivery_decision(&cfg, "warn", "zone.sensor_discrepancy"),
|
||||
DeliveryDecision::Silent("alert_type_disabled")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordinary_info_event_has_no_notification_status() {
|
||||
let cfg = NotificationSettings::default();
|
||||
assert_eq!(
|
||||
delivery_decision(&cfg, "info", "zone.quick_control"),
|
||||
DeliveryDecision::NotApplicable
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_pushover(
|
||||
|
||||
@@ -51,6 +51,7 @@ SELECT
|
||||
|
||||
pub const INSERT_EVENT: &str =
|
||||
"INSERT INTO event_log(timestamp,level,kind,message,metadata) VALUES(?1,?2,?3,?4,?5)";
|
||||
pub const UPDATE_EVENT_METADATA: &str = "UPDATE event_log SET metadata=?1 WHERE id=?2";
|
||||
pub const LIST_EVENTS: &str =
|
||||
"SELECT id,timestamp,level,kind,message,metadata FROM event_log ORDER BY id DESC LIMIT ?1";
|
||||
pub const PRUNE_EVENTS: &str = "DELETE FROM event_log WHERE timestamp < ?1";
|
||||
|
||||
+12
-4
@@ -165,12 +165,17 @@ impl AppState {
|
||||
}
|
||||
|
||||
pub fn log(&self, level: &str, kind: &str, message: &str, metadata: Value) {
|
||||
if let Err(err) = self.db.log_event(level, kind, message, &metadata) {
|
||||
tracing::warn!(error=?err, "cannot persist event log");
|
||||
}
|
||||
let event_id = match self.db.log_event(level, kind, message, &metadata) {
|
||||
Ok(id) => Some(id),
|
||||
Err(err) => {
|
||||
tracing::warn!(error=?err, "cannot persist event log");
|
||||
None
|
||||
}
|
||||
};
|
||||
self.broadcast(
|
||||
"log.created",
|
||||
serde_json::json!({
|
||||
"id": event_id,
|
||||
"level": level,
|
||||
"kind": kind,
|
||||
"message": message,
|
||||
@@ -183,7 +188,10 @@ impl AppState {
|
||||
let kind = kind.to_string();
|
||||
let message = message.to_string();
|
||||
handle.spawn(async move {
|
||||
crate::notifications::dispatch(state, level, kind, message, metadata).await;
|
||||
crate::notifications::dispatch(
|
||||
state, event_id, level, kind, message, metadata,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user