refactor: move host, docker, alert and monitor queries into service modules
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 35s
CI / unit (push) Successful in 59s
CI / publish (push) Successful in 56s

MCP-3 of milestone 461 "MCP server: Steward as an LLM-inspectable,
LLM-operable hub". The /mcp tools run outside Quart's request pipeline, so
the queries they share with the pages now live in plain functions that take
an open session:

- steward/hosts/service.py: uptime, status, monitors, agent metrics,
  agent overview (stale threshold passed in, not read from current_app),
  CPU sparks.
- plugins/docker/service.py: bucket, container_history, host_map,
  list_containers, container_events, query_logs.
- steward/alerts/service.py: active_alerts (also used by core/reports),
  list_rules, list_maintenance_windows.
- steward/monitors/service.py: monitor_rows (was routes._rows_data).

Routes are repointed and render the same data. One behavior fix rides
along: the container-log search now escapes % and _ so they match
literally instead of acting as LIKE wildcards.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 07:29:21 -04:00
co-authored by Claude Opus 5.5
parent 970adf41e7
commit 312ea95bcf
10 changed files with 486 additions and 397 deletions
+36
View File
@@ -0,0 +1,36 @@
"""Alert queries shared by the alerts pages and the MCP tools.
Each function takes an open session and returns rows, with no Quart request
context, so the routes and the /mcp tools (which run outside Quart's pipeline)
call the same code.
"""
from __future__ import annotations
from sqlalchemy import select
from steward.models.alerts import AlertRule, AlertState, AlertStateEnum, MaintenanceWindow
# States that need someone's attention: pending (breaching, not yet for long
# enough), firing, and acknowledged-but-still-breaching.
ACTIVE_STATES = (AlertStateEnum.firing, AlertStateEnum.acknowledged, AlertStateEnum.pending)
async def active_alerts(db) -> list:
"""(AlertState, AlertRule) rows not yet resolved, latest transition first."""
return list((await db.execute(
select(AlertState, AlertRule)
.join(AlertRule, AlertState.rule_id == AlertRule.id)
.where(AlertState.state.in_(ACTIVE_STATES))
.order_by(AlertState.last_transition_at.desc())
)).all())
async def list_rules(db) -> list[AlertRule]:
return list((await db.execute(select(AlertRule).order_by(AlertRule.name))).scalars())
async def list_maintenance_windows(db) -> list[MaintenanceWindow]:
"""Every window, latest start first."""
return list((await db.execute(
select(MaintenanceWindow).order_by(MaintenanceWindow.start_at.desc())
)).scalars())