Add tray status model
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
826daa3ab7
commit
715db9ec36
3 changed files with 94 additions and 0 deletions
2
enodia_sentinel/tray/__init__.py
Normal file
2
enodia_sentinel/tray/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
"""Optional desktop tray applet. GUI deps live behind the [tray] extra."""
|
||||||
49
enodia_sentinel/tray/state.py
Normal file
49
enodia_sentinel/tray/state.py
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
"""Pure status model for the tray applet. No GUI or subprocess imports."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class TrayState:
|
||||||
|
running: bool
|
||||||
|
total_alerts: int
|
||||||
|
high_alerts: int
|
||||||
|
last_alert: str | None
|
||||||
|
color: str # "green" | "amber" | "grey"
|
||||||
|
summary: str
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tooltip(self) -> str:
|
||||||
|
return f"Enodia Sentinel — {self.summary}"
|
||||||
|
|
||||||
|
|
||||||
|
def _high_count(counts: dict) -> int:
|
||||||
|
# Severity keys are stringified Severity names ("HIGH"/"CRITICAL"); be
|
||||||
|
# tolerant of case in case a producer lowercases them.
|
||||||
|
total = 0
|
||||||
|
for key, value in counts.items():
|
||||||
|
if str(key).upper() in ("HIGH", "CRITICAL"):
|
||||||
|
total += int(value)
|
||||||
|
return total
|
||||||
|
|
||||||
|
|
||||||
|
def parse_status(status: dict | None) -> TrayState:
|
||||||
|
if not status:
|
||||||
|
return TrayState(False, 0, 0, None, "grey", "Unknown")
|
||||||
|
running = bool(status.get("running"))
|
||||||
|
total = int(status.get("total_alerts", 0))
|
||||||
|
high = _high_count(status.get("counts") or {})
|
||||||
|
last = status.get("last_alert")
|
||||||
|
if not running:
|
||||||
|
return TrayState(False, total, high, last, "grey", "Stopped")
|
||||||
|
if high:
|
||||||
|
return TrayState(
|
||||||
|
True, total, high, last, "amber",
|
||||||
|
f"Running — {high} high/critical of {total} alerts",
|
||||||
|
)
|
||||||
|
if total:
|
||||||
|
return TrayState(True, total, high, last, "green",
|
||||||
|
f"Running — {total} alerts")
|
||||||
|
return TrayState(True, total, high, last, "green", "Running — no alerts")
|
||||||
43
tests/test_tray_state.py
Normal file
43
tests/test_tray_state.py
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
"""Tray status model: maps `status --json` to a TrayState."""
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from enodia_sentinel.tray.state import TrayState, parse_status
|
||||||
|
|
||||||
|
|
||||||
|
class TestParseStatus(unittest.TestCase):
|
||||||
|
def test_none_is_unknown_grey(self):
|
||||||
|
s = parse_status(None)
|
||||||
|
self.assertFalse(s.running)
|
||||||
|
self.assertEqual(s.color, "grey")
|
||||||
|
self.assertEqual(s.summary, "Unknown")
|
||||||
|
|
||||||
|
def test_stopped_is_grey(self):
|
||||||
|
s = parse_status({"running": False, "total_alerts": 3,
|
||||||
|
"counts": {"high": 1}})
|
||||||
|
self.assertFalse(s.running)
|
||||||
|
self.assertEqual(s.color, "grey")
|
||||||
|
self.assertEqual(s.summary, "Stopped")
|
||||||
|
|
||||||
|
def test_running_clean_is_green(self):
|
||||||
|
s = parse_status({"running": True, "total_alerts": 0, "counts": {}})
|
||||||
|
self.assertEqual(s.color, "green")
|
||||||
|
self.assertIn("no alerts", s.summary)
|
||||||
|
|
||||||
|
def test_running_with_medium_only_is_green(self):
|
||||||
|
s = parse_status({"running": True, "total_alerts": 2,
|
||||||
|
"counts": {"MEDIUM": 2}})
|
||||||
|
self.assertEqual(s.color, "green")
|
||||||
|
self.assertIn("2 alerts", s.summary)
|
||||||
|
|
||||||
|
def test_running_with_high_is_amber(self):
|
||||||
|
s = parse_status({"running": True, "total_alerts": 4,
|
||||||
|
"counts": {"HIGH": 1, "CRITICAL": 1, "MEDIUM": 2}})
|
||||||
|
self.assertEqual(s.high_alerts, 2)
|
||||||
|
self.assertEqual(s.color, "amber")
|
||||||
|
self.assertIn("high/critical", s.summary)
|
||||||
|
|
||||||
|
def test_tooltip_includes_summary(self):
|
||||||
|
s = parse_status({"running": True, "total_alerts": 0, "counts": {}})
|
||||||
|
self.assertIn(s.summary, s.tooltip)
|
||||||
|
self.assertIn("Enodia Sentinel", s.tooltip)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue