fluxer-admin-alerts/runner.py
Clarth 674d0df703 Add group_alerts setting for per-run summary notifications
Allow choosing between one alert per item (default) and one grouped summary alert per run when any items need attention via general.group_alerts.
2026-11-13 20:37:28 -05:00

103 lines
3.4 KiB
Python

from __future__ import annotations
import logging
from datetime import datetime, timezone
from config import Settings
from engine import Notice, decide
from fluxer_api import FluxerAPI, FluxerAPIError
from notifiers import NotifierError, Message, build_notifiers
from sources import SOURCES, Item
from state import StateStore
log = logging.getLogger(__name__)
KIND_PREFIX = {
"new": "New",
"reminder": "Still open",
"repeat": "Reminder",
"cleared": "Cleared",
}
def _to_message(notice: Notice, admin_ui_url: str) -> Message:
link = f"{admin_ui_url}{notice.path}" if notice.path else admin_ui_url
return Message(
title=f"{KIND_PREFIX[notice.kind]}: {notice.title}",
body=notice.detail,
link=link,
urgent=notice.urgent,
)
def _group_messages(notices: list[Notice], admin_ui_url: str) -> list[Message]:
if not notices:
return []
lines: list[str] = []
urgent = False
for notice in notices:
label = KIND_PREFIX[notice.kind]
detail = notice.detail.replace("\n", " / ")
lines.append(f"{label}: {notice.title} - {detail}")
urgent = urgent or notice.urgent
title = f"Admin alerts: {len(notices)} item{'s' if len(notices) != 1 else ''} need attention"
body = "\n".join(lines)
if len(body) > 4000:
body = body[:3997] + "..."
return [Message(title=title, body=body, link=admin_ui_url, urgent=urgent)]
def run(settings: Settings, dry_run: bool = False) -> int:
api = FluxerAPI(settings.api_url, settings.api_key, settings.request_timeout)
now = datetime.now(timezone.utc)
failures = 0
checked: set[str] = set()
seen: dict[str, tuple[str, Item]] = {}
for source in SOURCES:
cfg = settings.sources[source.name]
if not cfg.enabled:
log.debug("%s: disabled", source.name)
continue
try:
items = source.fetch(api, cfg)
except FluxerAPIError as exc:
log.error("%s: %s", source.name, exc)
failures += 1
continue
checked.add(source.name)
log.info("%s: %d open", source.name, len(items))
for item in items:
seen[item.key] = (source.name, item)
store = StateStore(settings.state_file)
try:
known = store.load()
plan = decide(now, seen, known, checked, settings.realert, settings.notify_cleared)
if settings.group_alerts:
messages = _group_messages(plan.notices, settings.admin_ui_url)
else:
messages = [_to_message(n, settings.admin_ui_url) for n in plan.notices]
if dry_run:
for message in messages:
log.info("would send: %s | %s | %s", message.title, message.body.replace("\n", " / "), message.link)
log.info("dry run: %d messages, %d state changes, nothing sent or saved", len(messages), len(plan.records))
return 1 if failures else 0
if messages:
try:
for notifier in build_notifiers(settings):
notifier.send(messages)
except NotifierError as exc:
log.error("%s; nothing was recorded, so these will be retried next run", exc)
return 1
log.info("sent %d messages", len(messages))
else:
log.info("nothing new to send")
store.save(plan.records)
finally:
store.close()
return 1 if failures else 0