v0.12.6-hotfix

This commit is contained in:
Mateusz Gruszczyński
2026-09-04 14:50:18 +02:00
parent 63e6e13a5d
commit 11da46c4d6
12 changed files with 291 additions and 29 deletions
+8
View File
@@ -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
View File
@@ -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
View File
@@ -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(
+1
View File
@@ -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
View File
@@ -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;
});
}
}