35f658b573
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
75 lines
3.4 KiB
HTML
75 lines
3.4 KiB
HTML
{# HTMX fragment — monitor rows (list page). #}
|
|
{% if monitor_data %}
|
|
{% for d in monitor_data %}
|
|
{% 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:160px;max-width:240px;">
|
|
<span class="badge" style="font-size:0.62rem;vertical-align:middle;">{{ m.type | upper }}</span>
|
|
{% if m.host_id %}
|
|
<a class="ping-name" href="/hosts/{{ m.host_id }}" title="{{ m.name }}"
|
|
style="color:inherit;text-decoration:none;">{{ m.name }}</a>
|
|
{% else %}
|
|
<span class="ping-name" title="{{ m.name }}">{{ m.name }}</span>
|
|
{% endif %}
|
|
<span class="ping-addr" title="{{ m.target }}">{{ m.target }}</span>
|
|
</div>
|
|
|
|
<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 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>
|
|
{% elif m.type == 'dns' and latest.resolved_ip %}
|
|
<span class="ping-cur-good" style="font-size:0.72rem;">{{ latest.resolved_ip }}</span>
|
|
{% else %}
|
|
<span class="ping-cur-good">UP</span>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div style="min-width:4.5rem;text-align:right;font-size:0.78rem;font-variant-numeric:tabular-nums;{{ threshold_style(d.uptime_pct, 'uptime') if d.uptime_pct is not none else 'color:var(--text-dim);' }}">
|
|
{% if d.uptime_pct is not none %}{{ "%.1f"|format(d.uptime_pct) }}%{% else %}—{% endif %}
|
|
<span style="color:var(--text-dim);font-size:0.7rem;margin-left:0.15rem;">{{ range_key }}</span>
|
|
</div>
|
|
|
|
{% if d.tls_days is not none %}
|
|
<div title="TLS certificate expiry" style="min-width:4rem;text-align:right;font-size:0.72rem;{{ 'color:var(--red);' if d.tls_days < 14 else ('color:var(--yellow);' if d.tls_days < 30 else 'color:var(--text-dim);') }}">
|
|
cert {{ d.tls_days|round|int }}d
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% 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" style="padding:0.2rem 0.5rem;font-size:0.72rem;">Edit</a>
|
|
<form method="post" action="/monitors/{{ m.id }}/toggle" style="display:inline;">
|
|
<button type="submit" class="btn btn-sm" style="padding:0.2rem 0.5rem;font-size:0.72rem;">
|
|
{{ "Pause" if m.enabled else "Resume" }}</button>
|
|
</form>
|
|
<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.9rem;padding:1rem 0;">
|
|
No monitors yet. Add one above, or attach checks to a host from the
|
|
<a href="/hosts/" style="color:var(--accent);">Hosts</a> page.
|
|
</p>
|
|
{% endif %}
|