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
+11 -11
View File
@@ -5,7 +5,7 @@ dc8d8412b74e19f2d32570ca0ef176c0036a140e47e41d020cb4d140ad9bfe39 ./Cargo.toml
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE 19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
079ddac7ba983a3d679b8c748bd43c731eb078e91a40c5d8a683bd76e14f06c5 ./README.md 079ddac7ba983a3d679b8c748bd43c731eb078e91a40c5d8a683bd76e14f06c5 ./README.md
d56cfff4179a6511aa764a91c6ddebe21fcd35f36b7298e62d012e11f62bfeb9 ./build.rs d56cfff4179a6511aa764a91c6ddebe21fcd35f36b7298e62d012e11f62bfeb9 ./build.rs
514065eefa7428afbcacaca7095605495f95339a2ae8b23707315dc434f9c4c5 ./docs/API.md ed543caace807645561db525c19ae2c3745600af146e1c1ea9b89a39a7f597a3 ./docs/API.md
2e1e18fd8167dabfe2469c26e85cce62486c7cb6a502c63c6f6b0cd74d5885e0 ./docs/FLOW.md 2e1e18fd8167dabfe2469c26e85cce62486c7cb6a502c63c6f6b0cd74d5885e0 ./docs/FLOW.md
cb62e2ea377bdf4785c139ee169eec4914e04ceac6883c0afcb4c96fe60427b0 ./docs/openapi.json cb62e2ea377bdf4785c139ee169eec4914e04ceac6883c0afcb4c96fe60427b0 ./docs/openapi.json
b11e581dd916676e24402112fe6896bc681abf3c94cafc85c4d3ded3f3a272a3 ./home-assistant/README.md b11e581dd916676e24402112fe6896bc681abf3c94cafc85c4d3ded3f3a272a3 ./home-assistant/README.md
@@ -27,8 +27,8 @@ cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./home-assista
13f30e2dcdcedbd1b6c3f99c2335e0487108fd72c8e86922368b84f2fa2038ae ./home-assistant/custom_components/gree_controller/translations/pl.json 13f30e2dcdcedbd1b6c3f99c2335e0487108fd72c8e86922368b84f2fa2038ae ./home-assistant/custom_components/gree_controller/translations/pl.json
c5fc5c87273d82d2834a0a5a365c821a416413ed509613441e24e4dc3507c4d2 ./home-assistant/generated/gree_controller_entities.example.json c5fc5c87273d82d2834a0a5a365c821a416413ed509613441e24e4dc3507c4d2 ./home-assistant/generated/gree_controller_entities.example.json
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md 253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
00cd78873d3a3290419a83ea736a9ec5f0d93f590159cbf40c54d5ea6c3c89fb ./lang/en.json a5412c879ac0896ae6f9c2d66dd861789897b8c8610098e3fb9b5462068acd74 ./lang/en.json
a088acc585a863e204100af7f5465a5d761ba94c0afff66b5ed329e7d204ec35 ./lang/pl.json 615d7297c5fa6228c017a7814aa17898aa990bfbde649d9c7c43b7b49d046ebe ./lang/pl.json
d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py
b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json
1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json 1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json
@@ -109,11 +109,11 @@ e0340b0e5945d5a38a1054e549c819566d5c1ef6034ab0e4d44c68a3b9a249cf ./src/db.rs
be4082b142fe16ba40c5978b287b7ef808e67c13edbd8f2d6637ccad974b8847 ./src/db/configuration.rs be4082b142fe16ba40c5978b287b7ef808e67c13edbd8f2d6637ccad974b8847 ./src/db/configuration.rs
bc10f1a66ddcaacbb08b096a02998482da547024dd0ba3144d00f993b4351cb0 ./src/db/core_devices.rs bc10f1a66ddcaacbb08b096a02998482da547024dd0ba3144d00f993b4351cb0 ./src/db/core_devices.rs
4c20289c08b0cb9c3c4324d9dbc6a47c7dd953aff6fd1746f04b6024043d3bc5 ./src/db/device_history.rs 4c20289c08b0cb9c3c4324d9dbc6a47c7dd953aff6fd1746f04b6024043d3bc5 ./src/db/device_history.rs
eb07d94b58bcc8ea17b5b086cc88c165a35666f84c3d9a1d74f72dc30b979829 ./src/db/events_tokens.rs 88cb4b7ff61a8a558949faa7157d4bc2c45facb951c57240412c4c9d3befdb86 ./src/db/events_tokens.rs
bb5048ee60053f39ce3c1203dc1f24c7047310538d2357f105a052c016d21ec4 ./src/db/flows.rs bb5048ee60053f39ce3c1203dc1f24c7047310538d2357f105a052c016d21ec4 ./src/db/flows.rs
5f4b567c6bc866d0674b3b0c66f4aeccf561774623f575b872ff25bf015b91c2 ./src/db/ha_history.rs 5f4b567c6bc866d0674b3b0c66f4aeccf561774623f575b872ff25bf015b91c2 ./src/db/ha_history.rs
c0a7fa3eac198737d7261ad00c542a614e6d7291f6581af8e7f7aef9dec1a049 ./src/db/schedules_automations.rs c0a7fa3eac198737d7261ad00c542a614e6d7291f6581af8e7f7aef9dec1a049 ./src/db/schedules_automations.rs
1a209dc6d8df864778bb03c980f309a2ec86fc62df274880fbb95eff8470174e ./src/db/tests.rs 12c4f69011b22ec0af65121100e5cc717fe7bd5a139f4180d1982a9045c6918e ./src/db/tests.rs
f90fbb14f29a20b799b3856bdaa22812a9f0d9dfa46693f1341c160dccda8a97 ./src/db/zone_history.rs f90fbb14f29a20b799b3856bdaa22812a9f0d9dfa46693f1341c160dccda8a97 ./src/db/zone_history.rs
4d2c6053586558afbca6b60fbbc9b1cd397f74f8284d7c01f3337eb42bf6d4f7 ./src/engine.rs 4d2c6053586558afbca6b60fbbc9b1cd397f74f8284d7c01f3337eb42bf6d4f7 ./src/engine.rs
6046bdbc9c02ac74f47068229c59af69abc8bfa46cfeff6fe7ba573b493e128c ./src/engine/automations.rs 6046bdbc9c02ac74f47068229c59af69abc8bfa46cfeff6fe7ba573b493e128c ./src/engine/automations.rs
@@ -152,7 +152,7 @@ e1621255217ee6128151cc898373dd404123cb6d54017beec6c7e3e5c3030486 ./src/models/i
887eb80a58998e90023e952c1fde67435eb843493387ae64536613b74c084534 ./src/models/settings_api.rs 887eb80a58998e90023e952c1fde67435eb843493387ae64536613b74c084534 ./src/models/settings_api.rs
c4de6101de85d9d7e6814b2504b87d9216561e3d9e274054f081f505fb152140 ./src/models/temporary_thermostat.rs c4de6101de85d9d7e6814b2504b87d9216561e3d9e274054f081f505fb152140 ./src/models/temporary_thermostat.rs
7d798228a8d4d4e60a112167c001535f4b8a5fa32ec7a33554c73125c42256c2 ./src/models/zone.rs 7d798228a8d4d4e60a112167c001535f4b8a5fa32ec7a33554c73125c42256c2 ./src/models/zone.rs
48e5d0e0c8b2863daf35c64ad49bf5333fb3b807568323cf0958efbc3aee7433 ./src/notifications.rs 62e696df8b86835df7ad127f03c257a78580769dc82bc66daa5041f71e8440a5 ./src/notifications.rs
cca742bb2b500eb9269f69385fcf88053cd08aabf6a98b7669b8a7a2fd67df13 ./src/protocol/crypto.rs cca742bb2b500eb9269f69385fcf88053cd08aabf6a98b7669b8a7a2fd67df13 ./src/protocol/crypto.rs
28969da06a454ddd1ebea5fa606e8446a83dfd21e21352767c189013c508cb90 ./src/protocol/gree.rs 28969da06a454ddd1ebea5fa606e8446a83dfd21e21352767c189013c508cb90 ./src/protocol/gree.rs
7907c33e6396c7e0d61705b735b21c84d6ed784012fb61022b872c6745540f1d ./src/protocol/gree/binding.rs 7907c33e6396c7e0d61705b735b21c84d6ed784012fb61022b872c6745540f1d ./src/protocol/gree/binding.rs
@@ -169,14 +169,14 @@ c868422c02ba457df21ae92acbd00698c243855ddd1d4e95ac2166796eba9a44 ./src/protocol
7fac65a9dcbcecee09816f5447c7792dc94f736b1efb66ac85629db75f6280be ./src/queries/device_history.rs 7fac65a9dcbcecee09816f5447c7792dc94f736b1efb66ac85629db75f6280be ./src/queries/device_history.rs
3fb50f57d30ac23ef122c45dae4bd96c7cb7184df02d99aef00d9890b66479e2 ./src/queries/entities.rs 3fb50f57d30ac23ef122c45dae4bd96c7cb7184df02d99aef00d9890b66479e2 ./src/queries/entities.rs
2c5fba462b72158b04dbaaa53c8536263f8d72a1338a6c4ff06cb28a36f6ae27 ./src/queries/ha_history.rs 2c5fba462b72158b04dbaaa53c8536263f8d72a1338a6c4ff06cb28a36f6ae27 ./src/queries/ha_history.rs
c91e4b8b250b30cbe76f313b70c16244e9003bce92e02c82283a1831f91974c5 ./src/queries/maintenance.rs 3112452367b68aeda0a5e3143b1a51ce62e3159859fb41e92c35642a3cd72322 ./src/queries/maintenance.rs
3a49bad74a2f047893e726b5e00a6e833c228ad268bdf50cd60f786ba164ea54 ./src/queries/schema.rs 3a49bad74a2f047893e726b5e00a6e833c228ad268bdf50cd60f786ba164ea54 ./src/queries/schema.rs
f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/zone_history.rs f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/zone_history.rs
008be170ed418f8701c512f521828b8e6c0071ff195cd6f7aa39fade6c7c3fa9 ./src/state.rs 7af6db4d826f8cb5f2305f743bffe4e112d4e1c1772366a83ba75953e60db1c3 ./src/state.rs
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html 544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
62e0a01a47a2d30e5265010aef5a089b0055d824fede940b7665d76f92018522 ./web/index.html 9e98df7a759bd76bc7682b23c3c4b7e29f7bde972f03ae316ba7f8159301e2c6 ./web/index.html
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js/README.md 9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js/README.md
45fb54541a117a8ee0dc484cd263b6af4aafab75d94d269b126bb603b5f0392c ./web/js/bootstrap.js 45fb54541a117a8ee0dc484cd263b6af4aafab75d94d269b126bb603b5f0392c ./web/js/bootstrap.js
badde8a5214a0322b2c45803c02a1cb723d53b25e57c687369cd804f509d9744 ./web/js/charts.js badde8a5214a0322b2c45803c02a1cb723d53b25e57c687369cd804f509d9744 ./web/js/charts.js
@@ -189,11 +189,11 @@ cf1ebf3b9d666f309a1add070b4da86be86d990b02c8d6474f6d121c6d332746 ./web/js/forms
dbf36223863ba882c03eccea4516ba8db7acf0c2e72c6c64490cb994f1e20f96 ./web/js/history.js dbf36223863ba882c03eccea4516ba8db7acf0c2e72c6c64490cb994f1e20f96 ./web/js/history.js
7eaa04992ea828dc89f5eaebf674cbbfd160f53eb8d0d6ef0d4e47bd8c28c435 ./web/js/main.js 7eaa04992ea828dc89f5eaebf674cbbfd160f53eb8d0d6ef0d4e47bd8c28c435 ./web/js/main.js
733fcc595ed9c03c9639acc6caba46f0588f29e0b95bf72891e34443e7aa9db8 ./web/js/navigation.js 733fcc595ed9c03c9639acc6caba46f0588f29e0b95bf72891e34443e7aa9db8 ./web/js/navigation.js
e3e01511a34175b88eb01dac2c557e8b112fcad5826f02a0bc3cd1e8dfb7ff91 ./web/js/realtime.js 0f0ecb9fcc2b47aece715de4ededdc1cf512daf404c37efe33bf9069027221e3 ./web/js/realtime.js
c8c82e88c3715b1bfabf155e36266a4c94f5e2fc03eb39dcdd9d08a1985a997c ./web/js/router.js c8c82e88c3715b1bfabf155e36266a4c94f5e2fc03eb39dcdd9d08a1985a997c ./web/js/router.js
25f5c9a0aa8622f419b05f64712c9eca4896f0e79818616904b83051fd77b07b ./web/js/settings-ui.js 25f5c9a0aa8622f419b05f64712c9eca4896f0e79818616904b83051fd77b07b ./web/js/settings-ui.js
7399a951ae585d7c9ee55cc57064c64a2806ec725e2456cfe6b09bc61f8f4166 ./web/js/settings.js 7399a951ae585d7c9ee55cc57064c64a2806ec725e2456cfe6b09bc61f8f4166 ./web/js/settings.js
6b653e4fe2d4db4ffae6ff8f39b8ce57a10a990dc0000ee4aae51d949afadd52 ./web/manifest.webmanifest 6b653e4fe2d4db4ffae6ff8f39b8ce57a10a990dc0000ee4aae51d949afadd52 ./web/manifest.webmanifest
801bb151f7b6220587e22c3114fb224a540dcfbddd1c09870007b7b893a01301 ./web/styles.css 0b9774bb388158e23f8c90297d70659a015e0db58d25eadcae316a49e3c9fcb7 ./web/styles.css
ae1b03f30b494f474a781a5d32072f1e73eba2b7c768109fc1cc8028cb6662a4 ./web/sw.js ae1b03f30b494f474a781a5d32072f1e73eba2b7c768109fc1cc8028cb6662a4 ./web/sw.js
d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./web/theme-init.js d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./web/theme-init.js
+2
View File
@@ -1012,6 +1012,8 @@ Response:
} }
``` ```
For events that are eligible for external notification, `metadata.notification` records the delivery result. `status` is `sent`, `silent` or `failed`. A `silent` event includes a `reason` such as `alert_type_disabled`, `notifications_disabled`, `mode_filtered` or `cooldown`; the Web UI shows these rows with a **SILENT** badge. Notification-status changes are also emitted as `log.updated` WebSocket events.
--- ---
## Runtime settings — 0.12.0 ## Runtime settings — 0.12.0
+7
View File
@@ -192,6 +192,12 @@
"logs.diagnostics": "Diagnostics", "logs.diagnostics": "Diagnostics",
"logs.emptyTitle": "No events", "logs.emptyTitle": "No events",
"logs.emptyText": "The event log is empty.", "logs.emptyText": "The event log is empty.",
"logs.silent": "SILENT",
"logs.silentHint": "No notification was sent.",
"logs.silentAlertTypeDisabled": "No notification was sent: this alert type is disabled.",
"logs.silentNotificationsDisabled": "No notification was sent: notifications are disabled.",
"logs.silentModeFiltered": "No notification was sent: the notification mode filtered this event.",
"logs.silentCooldown": "No notification was sent: notification cooldown is active.",
"common.name": "Name", "common.name": "Name",
"common.zone": "Zone", "common.zone": "Zone",
"common.device": "Device", "common.device": "Device",
@@ -877,6 +883,7 @@
"flow.emptyCanvas": "Start by adding blocks", "flow.emptyCanvas": "Start by adding blocks",
"flow.emptyCanvasHint": "Put conditions on the left and actions on the right.", "flow.emptyCanvasHint": "Put conditions on the left and actions on the right.",
"flow.interpretation": "Interpretation", "flow.interpretation": "Interpretation",
"flow.previewDetails": "Interpretation and Flow cycle",
"flow.selectBlock": "Select a block", "flow.selectBlock": "Select a block",
"flow.selectBlockHint": "Its settings will appear here.", "flow.selectBlockHint": "Its settings will appear here.",
"flow.newDefaultName": "New logic", "flow.newDefaultName": "New logic",
+7
View File
@@ -192,6 +192,12 @@
"logs.diagnostics": "Diagnostyka", "logs.diagnostics": "Diagnostyka",
"logs.emptyTitle": "Brak zdarzeń", "logs.emptyTitle": "Brak zdarzeń",
"logs.emptyText": "Log jest pusty.", "logs.emptyText": "Log jest pusty.",
"logs.silent": "SILENT",
"logs.silentHint": "Powiadomienie nie zostało wysłane.",
"logs.silentAlertTypeDisabled": "Powiadomienie nie zostało wysłane: ten typ alertu jest wyłączony.",
"logs.silentNotificationsDisabled": "Powiadomienie nie zostało wysłane: powiadomienia są wyłączone.",
"logs.silentModeFiltered": "Powiadomienie nie zostało wysłane: zdarzenie odfiltrował tryb powiadomień.",
"logs.silentCooldown": "Powiadomienie nie zostało wysłane: aktywny jest cooldown.",
"common.name": "Nazwa", "common.name": "Nazwa",
"common.zone": "Strefa", "common.zone": "Strefa",
"common.device": "Urządzenie", "common.device": "Urządzenie",
@@ -877,6 +883,7 @@
"flow.emptyCanvas": "Zacznij od dodania bloków", "flow.emptyCanvas": "Zacznij od dodania bloków",
"flow.emptyCanvasHint": "Warunki układaj po lewej, akcje po prawej.", "flow.emptyCanvasHint": "Warunki układaj po lewej, akcje po prawej.",
"flow.interpretation": "Interpretacja", "flow.interpretation": "Interpretacja",
"flow.previewDetails": "Interpretacja i cykl Flow",
"flow.selectBlock": "Wybierz blok", "flow.selectBlock": "Wybierz blok",
"flow.selectBlockHint": "Tutaj pojawią się jego ustawienia.", "flow.selectBlockHint": "Tutaj pojawią się jego ustawienia.",
"flow.newDefaultName": "Nowa logika", "flow.newDefaultName": "Nowa logika",
+8
View File
@@ -29,6 +29,14 @@ impl Db {
Ok(conn.last_insert_rowid()) 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>> { pub fn list_events(&self, limit: u32) -> Result<Vec<EventLog>> {
let conn = self.lock()?; let conn = self.lock()?;
let mut stmt = conn.prepare(queries::LIST_EVENTS)?; 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(); let loaded = db.get_device(&device.id).unwrap().unwrap();
assert_eq!(loaded.mac, device.mac); assert_eq!(loaded.mac, device.mac);
assert_eq!(db.list_devices().unwrap().len(), 1); 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(); .unwrap();
assert_eq!(db.list_events(10).unwrap().len(), 1); 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(); let conn = db.lock().unwrap();
conn.execute( conn.execute(
+133 -9
View File
@@ -64,14 +64,28 @@ fn alert_type_enabled(cfg: &NotificationSettings, kind: &str) -> bool {
types.other types.other
} }
fn should_send(cfg: &NotificationSettings, level: &str, kind: &str) -> bool { #[derive(Debug, Clone, Copy, PartialEq, Eq)]
if !cfg.enabled || !alert_type_enabled(cfg, kind) { enum DeliveryDecision {
return false; 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" { if !cfg.enabled {
return true; 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 { 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 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( pub async fn dispatch(
state: AppState, state: AppState,
event_id: Option<i64>,
level: String, level: String,
kind: String, kind: String,
message: String, message: String,
metadata: Value, metadata: Value,
) { ) {
let cfg = state.settings.read().await.notifications.clone(); 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; return;
} }
let title = format!( let title = format!(
@@ -139,8 +223,26 @@ pub async fn dispatch(
} }
_ => Err("unsupported notification provider".into()), _ => Err("unsupported notification provider".into()),
}; };
if let Err(err) = result { match result {
tracing::warn!(provider=%cfg.provider, error=%err, "notification delivery failed"); 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.sensor_discrepancy"));
assert!(!alert_type_enabled(&cfg, "zone.action_error")); 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( async fn send_pushover(
+1
View File
@@ -51,6 +51,7 @@ SELECT
pub const INSERT_EVENT: &str = pub const INSERT_EVENT: &str =
"INSERT INTO event_log(timestamp,level,kind,message,metadata) VALUES(?1,?2,?3,?4,?5)"; "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 = pub const LIST_EVENTS: &str =
"SELECT id,timestamp,level,kind,message,metadata FROM event_log ORDER BY id DESC LIMIT ?1"; "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"; 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) { pub fn log(&self, level: &str, kind: &str, message: &str, metadata: Value) {
if let Err(err) = self.db.log_event(level, kind, message, &metadata) { let event_id = match self.db.log_event(level, kind, message, &metadata) {
tracing::warn!(error=?err, "cannot persist event log"); Ok(id) => Some(id),
} Err(err) => {
tracing::warn!(error=?err, "cannot persist event log");
None
}
};
self.broadcast( self.broadcast(
"log.created", "log.created",
serde_json::json!({ serde_json::json!({
"id": event_id,
"level": level, "level": level,
"kind": kind, "kind": kind,
"message": message, "message": message,
@@ -183,7 +188,10 @@ impl AppState {
let kind = kind.to_string(); let kind = kind.to_string();
let message = message.to_string(); let message = message.to_string();
handle.spawn(async move { handle.spawn(async move {
crate::notifications::dispatch(state, level, kind, message, metadata).await; crate::notifications::dispatch(
state, event_id, level, kind, message, metadata,
)
.await;
}); });
} }
} }
+2 -2
View File
@@ -891,8 +891,8 @@
</div> </div>
<div class="flow-natural-preview" id="flowNaturalPreview"> <div class="flow-natural-preview" id="flowNaturalPreview">
<button type="button" class="flow-preview-toggle" data-action="flow-preview-toggle" <button type="button" class="flow-preview-toggle" data-action="flow-preview-toggle"
aria-expanded="false"><strong data-i18n="flow.interpretation">Interpretation</strong><span aria-expanded="false"><strong data-i18n="flow.previewDetails">Interpretation and Flow cycle</strong><span
class="flow-preview-toggle-icon" aria-hidden="true"></span></button> class="flow-preview-toggle-icon" aria-hidden="true"></span></button>
<div class="flow-preview-details"> <div class="flow-preview-details">
<div class="flow-preview-item flow-preview-interpretation"><strong <div class="flow-preview-item flow-preview-interpretation"><strong
data-i18n="flow.interpretation">Interpretation</strong><span id="flowInterpretation"></span></div> data-i18n="flow.interpretation">Interpretation</strong><span id="flowInterpretation"></span></div>
+14 -1
View File
@@ -1,3 +1,15 @@
function logNotificationBadge(item) {
const notification = item?.metadata?.notification;
if (notification?.status !== 'silent') return '';
const reasonKey = {
alert_type_disabled: 'logs.silentAlertTypeDisabled',
notifications_disabled: 'logs.silentNotificationsDisabled',
mode_filtered: 'logs.silentModeFiltered',
cooldown: 'logs.silentCooldown',
}[notification.reason] || 'logs.silentHint';
return `<span class="badge silent log-notification-badge" title="${esc(tr(reasonKey))}">${esc(tr('logs.silent'))}</span>`;
}
async function loadLogs() { async function loadLogs() {
renderLogRetention(); renderLogRetention();
try { try {
@@ -5,7 +17,7 @@ async function loadLogs() {
const level = $('#logLevelFilter')?.value || 'all', category = $('#logCategoryFilter')?.value || 'all'; const level = $('#logLevelFilter')?.value || 'all', category = $('#logCategoryFilter')?.value || 'all';
const logs = (data.events || []).filter(item => (level === 'all' || item.level === level) && (category === 'all' || logCategory(item.kind) === category)); const logs = (data.events || []).filter(item => (level === 'all' || item.level === level) && (category === 'all' || logCategory(item.kind) === category));
$('#logList').innerHTML = logs.length $('#logList').innerHTML = logs.length
? logs.map(item => `<div class="log-row ${esc(item.level)} category-${esc(logCategory(item.kind))}"><time>${esc(new Date(item.timestamp).toLocaleTimeString(locale()))}</time><span class="log-category">${esc(logCategory(item.kind))}</span><span class="kind">${esc(item.kind)}</span><span class="message">${esc(item.message)}</span></div>`).join('') ? logs.map(item => `<div class="log-row ${esc(item.level)} category-${esc(logCategory(item.kind))}"><time>${esc(new Date(item.timestamp).toLocaleTimeString(locale()))}</time><span class="log-category">${esc(logCategory(item.kind))}</span><span class="kind">${esc(item.kind)}${logNotificationBadge(item)}</span><span class="message">${esc(item.message)}</span></div>`).join('')
: `<div class="empty"><strong>${esc(tr('logs.emptyTitle'))}</strong>${esc(tr('logs.emptyText'))}</div>`; : `<div class="empty"><strong>${esc(tr('logs.emptyTitle'))}</strong>${esc(tr('logs.emptyText'))}</div>`;
} catch (error) { toast(error.message, true); } } catch (error) { toast(error.message, true); }
} }
@@ -70,6 +82,7 @@ async function handleWebSocketMessage(event) {
else if (message.event === 'gree.frame') { if (app.settings?.debug?.overlay_enabled) debugLine('GREE', `${data.direction || '?'} ${data.protocol_version || ''}`, data.device_name || data.device_id || data.target || '', message.timestamp, data.payload); } else if (message.event === 'gree.frame') { if (app.settings?.debug?.overlay_enabled) debugLine('GREE', `${data.direction || '?'} ${data.protocol_version || ''}`, data.device_name || data.device_id || data.target || '', message.timestamp, data.payload); }
else if (message.event === 'api.request') { if (app.settings?.debug?.overlay_enabled) debugLine('HTTP', `${data.method || '?'} ${data.status || ''}`, `${data.path || ''} · ${data.duration_ms ?? '?'} ms`, message.timestamp); } else if (message.event === 'api.request') { if (app.settings?.debug?.overlay_enabled) debugLine('HTTP', `${data.method || '?'} ${data.status || ''}`, `${data.path || ''} · ${data.duration_ms ?? '?'} ms`, message.timestamp); }
else if (message.event === 'log.created') { if (app.settings?.debug?.overlay_enabled) debugLine('API', data.kind || data.level || 'log', data.message || '', message.timestamp, data.metadata); if (app.currentView === 'logs') loadLogs(); } else if (message.event === 'log.created') { if (app.settings?.debug?.overlay_enabled) debugLine('API', data.kind || data.level || 'log', data.message || '', message.timestamp, data.metadata); if (app.currentView === 'logs') loadLogs(); }
else if (message.event === 'log.updated') { if (app.currentView === 'logs') loadLogs(); }
} catch (_) { } } catch (_) { }
} }
+83 -1
View File
@@ -9255,4 +9255,86 @@ body.flow-editor-open {
display: none; display: none;
content: none; content: none;
} }
} }
/* Notification delivery audit in Events. */
.log-row .kind .log-notification-badge {
margin-left: .45rem;
padding: 3px 7px;
border: 1px solid var(--line-strong);
background: transparent;
color: var(--muted);
font-size: .64rem;
font-weight: 700;
letter-spacing: .06em;
vertical-align: middle;
}
/* Keep the Flow footer compact before the full mobile layout kicks in. */
@media (min-width:761px) and (max-width:1200px) {
.flow-natural-preview {
display: block;
min-height: 0;
padding: 0;
overflow: hidden;
}
.flow-preview-toggle {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
min-height: 40px;
padding: 8px 14px;
border: 0;
border-radius: 0;
background: var(--surface);
color: var(--text);
text-align: left;
}
.flow-preview-toggle strong {
font-size: .8rem;
}
.flow-preview-toggle-icon {
color: var(--muted);
font-size: 1rem;
transition: transform .15s ease;
}
.flow-preview-details {
display: none;
flex-direction: column;
align-items: stretch;
gap: 8px;
max-height: 32dvh;
padding: 9px 14px 11px;
overflow: auto;
}
.flow-natural-preview.is-expanded .flow-preview-details {
display: flex;
}
.flow-natural-preview.is-expanded .flow-preview-toggle {
border-bottom: 1px solid var(--line);
}
.flow-natural-preview.is-expanded .flow-preview-toggle-icon {
transform: rotate(180deg);
}
.flow-preview-item {
grid-template-columns: auto minmax(0, 1fr);
gap: 10px;
}
.flow-preview-runtime {
justify-content: start;
text-align: left;
}
.flow-preview-runtime span {
white-space: normal;
}
}