Traefik: routers are graded from their latest scrape (router_state: crit on any real 5xx share, warn on p95 past the latency critical cutoff or a high 4xx share). Failing and slow routers lead as cards that say why; healthy ones fold into one table that stays open or shut across polls. Proxy counts are stat tiles; certificates are graded by Settings -> Thresholds (tls_days). UniFi: WAN health is a stat strip (latency graded by the thresholds), devices carry status dots, event categories and firewall actions are badges, the alarm count reuses .count-badge (renamed from .nav-count). SNMP: each device wears a status badge and its readings are stat tiles. Agent fleet: the table comes first, with a Status column (Reporting / Silent / Never reported, by the host page's stale_after_seconds), silent agents first, hosts linked, last seen as |ago. Audit: entries name their entity (one query per type, steward/audit/ service.py) and link it; something deleted since keeps the name its entry recorded and says "(deleted)". Tokens: charts read colours through chart-colors.js (stewardColor resolves a token to the rgb the browser computed; Chart.js defaults take the page's text and border colours), so host, history, Traefik and SNMP charts drop their hex. Template hex colours reach 0 (HEX_CEILING = 0), the last rgba pills go, and heavy weights (600/700/bold, 60 of them) become 500, the house pair being 400/500; a guard keeps templates there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
85 lines
3.7 KiB
Python
85 lines
3.7 KiB
Python
"""Name the thing each audit entry is about.
|
|
|
|
An audit row stores an entity type and id; the page shows the entity's
|
|
current name, linked to its page, in one query per type for the whole page.
|
|
An entity deleted since keeps whatever name the entry's own detail recorded.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from sqlalchemy import select
|
|
|
|
from steward.ansible.inventory_gen import run_title, scope_labels
|
|
from steward.models.alerts import AlertRule, MaintenanceWindow
|
|
from steward.models.ansible import AnsibleRun
|
|
from steward.models.ansible_inventory import AnsibleGroup, AnsibleTarget
|
|
from steward.models.ansible_schedule import AnsibleSchedule
|
|
from steward.models.api_tokens import ApiToken
|
|
from steward.models.hosts import Host
|
|
from steward.models.monitors import Monitor
|
|
|
|
# entity_type -> (model, its page for an id). A type named by a value rather
|
|
# than a row (plugin, ansible_playbook) is its own label.
|
|
_NAMED = {
|
|
"host": (Host, "/hosts/{}"),
|
|
"monitor": (Monitor, "/monitors/{}"),
|
|
"alert_rule": (AlertRule, "/alerts/history?rule={}"),
|
|
"maintenance_window": (MaintenanceWindow, "/alerts/maintenance"),
|
|
"ansible_target": (AnsibleTarget, "/ansible/inventory/targets/{}"),
|
|
"ansible_group": (AnsibleGroup, "/ansible/inventory/groups/{}"),
|
|
"ansible_schedule": (AnsibleSchedule, "/ansible/schedules"),
|
|
"api_token": (ApiToken, "/ai-access/"),
|
|
}
|
|
_SELF_NAMED = {"plugin": "/settings/plugins/", "ansible_playbook": None}
|
|
|
|
|
|
def _recorded_name(detail: dict) -> str | None:
|
|
"""A name the entry recorded when it was written, for a deleted entity."""
|
|
for key in ("name", "new_name", "username", "playbook"):
|
|
if detail.get(key):
|
|
return str(detail[key])
|
|
return None
|
|
|
|
|
|
async def entity_labels(db, items: list[dict]) -> dict[tuple[str, str], dict]:
|
|
"""{(entity_type, entity_id): {"label", "url", "gone"}} for the audit rows
|
|
in `items` ({"event", "detail"}), batched one query per type."""
|
|
ids: dict[str, set[str]] = {}
|
|
for it in items:
|
|
e = it["event"]
|
|
if e.entity_type and e.entity_id:
|
|
ids.setdefault(e.entity_type, set()).add(str(e.entity_id))
|
|
|
|
labels: dict[tuple[str, str], dict] = {}
|
|
for etype, wanted in ids.items():
|
|
if etype in _NAMED:
|
|
model, page = _NAMED[etype]
|
|
key_col = model.id
|
|
keys = [int(i) for i in wanted if i.isdigit()] if etype == "api_token" else list(wanted)
|
|
if not keys:
|
|
continue
|
|
for row_id, name in (await db.execute(
|
|
select(key_col, model.name).where(key_col.in_(keys)))).all():
|
|
labels[(etype, str(row_id))] = {"label": name, "url": page.format(row_id), "gone": False}
|
|
elif etype == "ansible_run":
|
|
runs = (await db.execute(
|
|
select(AnsibleRun).where(AnsibleRun.id.in_(list(wanted))))).scalars().all()
|
|
scopes = await scope_labels(db, [r.inventory_scope for r in runs])
|
|
for r in runs:
|
|
labels[(etype, r.id)] = {
|
|
"label": run_title(r.playbook_path, scopes.get(r.inventory_scope, r.inventory_scope)),
|
|
"url": f"/ansible/runs/{r.id}", "gone": False}
|
|
elif etype in _SELF_NAMED:
|
|
for i in wanted:
|
|
labels[(etype, i)] = {"label": i, "url": _SELF_NAMED[etype], "gone": False}
|
|
|
|
# What a lookup did not find was deleted since: keep the name the entry recorded.
|
|
for it in items:
|
|
e = it["event"]
|
|
if not (e.entity_type and e.entity_id):
|
|
continue
|
|
key = (e.entity_type, str(e.entity_id))
|
|
if key not in labels:
|
|
recorded = _recorded_name(it["detail"] or {})
|
|
labels[key] = {"label": recorded or f"{str(e.entity_id)[:8]}", "url": None, "gone": True}
|
|
return labels
|