feat(alerts): a rule can fire when a reading rises, "rose by N" (#6724)

A rule compared a reading with a fixed number, so Synology Disk 1's bad
sectors going from 3,160 to 3,392 could say nothing: a count that only grows
is soon past any line. "rose by" compares each reading with the one before it,
kept on the alert's state and only for such a rule. A fall (a replaced disk)
doesn't fire. On the rule form, through MCP, and worded in emails, the alert
sentence, history and the timeline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-30 19:03:29 -04:00
co-authored by Claude Opus 5.5
parent d83f8ffe9b
commit e51d969c43
15 changed files with 133 additions and 12 deletions
+10
View File
@@ -14,6 +14,14 @@ class AlertOperator(str, enum.Enum):
lte = "<="
eq = "=="
ne = "!="
# The reading against the one before it, not against a fixed line: for a
# count that only grows, where any fixed line is soon behind it.
rose = "rose"
@property
def said(self) -> str:
"""The operator as a condition reads: "> 90", "rose by 1"."""
return "rose by" if self is AlertOperator.rose else self.value
class AlertStateEnum(str, enum.Enum):
@@ -96,6 +104,8 @@ class AlertState(Base):
last_sample_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
sample_interval_s: Mapped[float | None] = mapped_column(Float, nullable=True)
no_data_since: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
# The reading before this one, kept only for a rule that fires on a rise.
last_value: Mapped[float | None] = mapped_column(Float, nullable=True)
class MaintenanceWindow(Base):