feat(monitors): unify ping/dns/http into one Monitor entity + custom targets
Collapse the three former check types into a single core `Monitor` entity
with one management surface (/monitors), one result table (monitor_results),
and a single scheduled task. Every type can now watch a free-standing custom
destination (optional host_id) — not just a registered Host.
- models: Monitor + MonitorResult replace PingResult/DnsResult; Host loses its
ping/dns facet columns (now Monitor rows linked by host_id).
- checks: monitors/{ping,dns,http}.py pure probes + runner.run_monitor
dispatcher; one monitor_check scheduler with a per-monitor due-filter.
- status: single monitor_status_source replaces the three sources.
- UI: /monitors blueprint (type-aware add/edit/list/widget); host hub shows a
host's linked monitors + "add monitor for this host"; nav + widget registry
+ alert metric catalog rewired. http plugin folded into core and removed.
- migration 0022 merges the http branch, data-migrates host facets +
http_monitors + all three result histories, drops the old tables/columns.
Resolves the per-host ping/dns auto-attach issue (#275): monitors are now
explicit, never auto-added to every host.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Jg27rgypiW2efULXJDtMC
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "_macros.html" import crumbs %}
|
||||
{% from "monitors/_fields.html" import type_fields, toggle_script %}
|
||||
{% block title %}{{ host.name }} — Hosts — Steward{% endblock %}
|
||||
{% block breadcrumb %}{{ crumbs([("Hosts", "/hosts/"), (host.name, "")]) }}{% endblock %}
|
||||
{% block content %}
|
||||
@@ -22,56 +23,97 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% set inp = "width:100%;padding:0.4rem 0.65rem;background:var(--bg);border:1px solid var(--border-mid);border-radius:4px;color:var(--text);font-size:0.88rem;" %}
|
||||
{% set lbl = "font-size:0.75rem;color:var(--text-muted);display:block;margin-bottom:0.2rem;" %}
|
||||
|
||||
{# ── Monitors ─────────────────────────────────────────────────────────────── #}
|
||||
<div class="card">
|
||||
<div style="display:flex;align-items:baseline;justify-content:space-between;margin-bottom:0.75rem;">
|
||||
<div style="display:flex;align-items:baseline;justify-content:space-between;margin-bottom:0.5rem;">
|
||||
<h3 class="section-title" style="margin-bottom:0;">Monitors</h3>
|
||||
<a href="/hosts/{{ host.id }}/edit" class="btn btn-ghost btn-sm" style="font-size:0.78rem;">Configure →</a>
|
||||
{% if uptime %}
|
||||
<span style="font-size:0.8rem;color:var(--text-muted);">
|
||||
uptime 24h <strong>{{ ("%.1f%%"|format(uptime['24h'])) if uptime['24h'] is not none else '—' }}</strong>
|
||||
· 7d <strong>{{ ("%.1f%%"|format(uptime['7d'])) if uptime['7d'] is not none else '—' }}</strong>
|
||||
· 30d <strong>{{ ("%.1f%%"|format(uptime['30d'])) if uptime['30d'] is not none else '—' }}</strong>
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="display:flex;gap:2rem;flex-wrap:wrap;">
|
||||
<div>
|
||||
<div style="font-size:0.75rem;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;">Ping</div>
|
||||
{% if not host.ping_enabled %}
|
||||
<div style="color:var(--text-dim);">off</div>
|
||||
{% elif ping is none %}
|
||||
<div style="color:var(--text-dim);">no data yet</div>
|
||||
{% else %}
|
||||
<div style="color:{{ 'var(--green)' if ping.status.value == 'up' else 'var(--red)' }};font-weight:600;">
|
||||
{{ ping.status.value }}
|
||||
</div>
|
||||
{% if ping.response_time_ms is not none %}
|
||||
<div style="font-size:0.8rem;color:var(--text-muted);">{{ '%.0f'|format(ping.response_time_ms) }} ms</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if monitors %}
|
||||
{% for d in monitors %}
|
||||
{% set m = d.monitor %}
|
||||
{% set latest = d.latest %}
|
||||
<div class="ping-row" {% if not m.enabled %}style="opacity:0.5;"{% endif %}>
|
||||
<div class="ping-meta" style="min-width:150px;max-width:230px;">
|
||||
<span class="badge" style="font-size:0.62rem;">{{ m.type | upper }}</span>
|
||||
<span class="ping-name" title="{{ m.name }}">{{ m.name }}</span>
|
||||
<span class="ping-addr" title="{{ m.target }}">{{ m.target }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:0.75rem;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;">DNS</div>
|
||||
{% if not host.dns_enabled %}
|
||||
<div style="color:var(--text-dim);">off</div>
|
||||
{% elif dns is none %}
|
||||
<div style="color:var(--text-dim);">no data yet</div>
|
||||
{% else %}
|
||||
<div style="color:{{ 'var(--green)' if dns.status.value == 'resolved' else 'var(--red)' }};font-weight:600;">
|
||||
{{ dns.status.value }}
|
||||
</div>
|
||||
{% if dns.resolved_ip %}
|
||||
<div style="font-size:0.8rem;color:var(--text-muted);font-family:ui-monospace,monospace;">{{ dns.resolved_ip }}</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div class="ping-pills">
|
||||
{% for _ in range([30 - d.heartbeat|length, 0]|max) %}
|
||||
<span class="pill pill-empty" title="No data"></span>
|
||||
{% endfor %}
|
||||
{% for h in d.heartbeat %}
|
||||
<span class="pill" title="{{ h.title }}"
|
||||
style="background:{{ '#1a6632' if h.state == 'up' else '#6a1515' }};"></span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:0.75rem;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;">Uptime</div>
|
||||
{% if uptime %}
|
||||
<div style="font-size:0.85rem;">
|
||||
24h <strong>{{ uptime['24h'] if uptime['24h'] is not none else '—' }}{{ '%' if uptime['24h'] is not none }}</strong>
|
||||
· 7d <strong>{{ uptime['7d'] if uptime['7d'] is not none else '—' }}{{ '%' if uptime['7d'] is not none }}</strong>
|
||||
· 30d <strong>{{ uptime['30d'] if uptime['30d'] is not none else '—' }}{{ '%' if uptime['30d'] is not none }}</strong>
|
||||
</div>
|
||||
{% else %}
|
||||
<div style="color:var(--text-dim);">—</div>
|
||||
{% endif %}
|
||||
<div class="ping-cur">
|
||||
{% if latest is none %}<span class="ping-cur-nd">—</span>
|
||||
{% elif not latest.is_up %}<span class="ping-cur-down">DOWN</span>
|
||||
{% elif latest.response_ms is not none %}
|
||||
<span style="{{ threshold_style(latest.response_ms, 'latency') or 'color:var(--green);' }}">{{ "%.0f"|format(latest.response_ms) }} ms</span>
|
||||
{% else %}<span class="ping-cur-good">UP</span>{% endif %}
|
||||
</div>
|
||||
{% if session.user_role in ['operator', 'admin'] %}
|
||||
<div style="display:flex;gap:0.35rem;flex-shrink:0;">
|
||||
<a href="/monitors/{{ m.id }}/edit" class="btn btn-sm btn-ghost" style="padding:0.2rem 0.5rem;font-size:0.72rem;">Edit</a>
|
||||
<form method="post" action="/monitors/{{ m.id }}/delete" style="display:inline;"
|
||||
onsubmit="return confirm('Delete monitor {{ m.name }}?');">
|
||||
<button type="submit" class="btn btn-sm" style="padding:0.2rem 0.5rem;font-size:0.72rem;color:var(--red);">✕</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p style="color:var(--text-muted);font-size:0.88rem;margin:0.25rem 0 0.75rem;">
|
||||
No monitors yet for this host. Add one below.
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% if session.user_role in ['operator', 'admin'] %}
|
||||
<details style="margin-top:0.75rem;">
|
||||
<summary style="cursor:pointer;font-size:0.85rem;color:var(--text-muted);">+ Add monitor for this host</summary>
|
||||
<form method="post" action="/monitors/add" style="display:grid;gap:0.6rem;margin-top:0.75rem;">
|
||||
<input type="hidden" name="host_id" value="{{ host.id }}">
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:0.6rem;align-items:end;">
|
||||
<div>
|
||||
<label style="{{ lbl }}">Type</label>
|
||||
<select name="type" id="mtype" onchange="mtoggle(this.value)" style="{{ inp }}">
|
||||
{% for t in monitor_types %}
|
||||
<option value="{{ t.value }}" {% if t.value == 'icmp' %}selected{% endif %}>{{ t.value | upper }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style="{{ lbl }}">Name</label>
|
||||
<input type="text" name="name" placeholder="optional" autocomplete="off" style="{{ inp }}">
|
||||
</div>
|
||||
<div>
|
||||
<label style="{{ lbl }}">Target</label>
|
||||
<input type="text" name="target" value="{{ host.address }}" autocomplete="off" required style="{{ inp }}">
|
||||
</div>
|
||||
<div>
|
||||
<label style="{{ lbl }}">Interval (s, 0=global)</label>
|
||||
<input type="number" name="check_interval_seconds" value="0" min="0" style="{{ inp }}">
|
||||
</div>
|
||||
{{ type_fields() }}
|
||||
</div>
|
||||
<div><button type="submit" class="btn btn-sm">Add Monitor</button></div>
|
||||
</form>
|
||||
</details>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# ── Agent (host_agent plugin fragment, embedded across the plugin boundary) ── #}
|
||||
@@ -168,4 +210,6 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{{ toggle_script() }}
|
||||
<script>var _mt=document.getElementById('mtype'); if(_mt) mtoggle(_mt.value);</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user