printers: low-toner alerts with configurable thresholds + support-team routing

Poll Zabbix for toner levels on a schedule and email/webhook on a downward
crossing. Warning fires at or below the warning threshold (default 5%),
critical at the critical threshold (default 0%); both thresholds are settings.
State lives in printersupplyalerts so an alert fires once per crossing and
re-arms after a refill.

Recipients mirror the printedparts pattern: plugin-scoped shopdb users +
roles + free-text emails (falling back to the site alert_recipients), and a
chosen support team's webhook (falling back to the site alert_webhook_url).

- PrinterSupplyAlert model + migration printers0002supplyalerts
- alerttier(remaining, warning, critical) + check_supplies poller
- flask printers check-toner-alerts CLI (run via scheduled task/cron)
- printers alert settings + Low-Toner Alerts settings page
- 7 tests: tier boundaries, once-per-crossing + re-arm, toner-only scope,
  custom thresholds, support-team webhook routing
This commit is contained in:
cproudlock
2026-07-22 14:47:59 -04:00
parent fb188fd302
commit b211e817d5
10 changed files with 739 additions and 3 deletions

View File

@@ -8,12 +8,14 @@ from .model_supply import ( # data-driven model -> toner/drum/waste mapping
SUPPLY_COLORS,
CAPACITY_TIERS,
)
from .supply_alert import PrinterSupplyAlert # per-printer toner alert state
__all__ = [
'Printer',
'PrinterType',
'PrinterDriver',
'ModelSupply',
'PrinterSupplyAlert',
'SUPPLY_TYPES',
'SUPPLY_COLORS',
'CAPACITY_TIERS',

View File

@@ -0,0 +1,41 @@
"""Per-printer supply alert state.
The supply poller (flask printers check-toner-alerts) is stateless: it reads
live levels from Zabbix each run. To alert once per downward crossing (and
re-arm after a refill) it needs to remember the last tier it alerted for each
printer + supply. One row per (printerid, supplykey); supplykey is the toner
color (black/cyan/magenta/yellow) or the raw item name when color is unknown.
"""
from shopdb.extensions import db
from shopdb.core.models.base import BaseModel
class PrinterSupplyAlert(BaseModel):
"""Last-alerted tier for one printer supply. lasttier in ok/warning/critical."""
__tablename__ = 'printersupplyalerts'
printersupplyalertid = db.Column(db.Integer, primary_key=True)
printerid = db.Column(
db.Integer,
db.ForeignKey('printers.printerid', ondelete='CASCADE'),
nullable=False,
index=True,
)
supplykey = db.Column(
db.String(64),
nullable=False,
comment='Toner color, or Zabbix item name when color is unknown',
)
lasttier = db.Column(
db.String(16),
nullable=False,
default='ok',
comment='Last tier alerted: ok, warning, or critical',
)
__table_args__ = (
db.UniqueConstraint('printerid', 'supplykey',
name='uq_printersupplyalert_printer_key'),
)