feat: live ping pill widget (dashboard + /ping/ page with threshold settings)
This commit is contained in:
@@ -86,6 +86,7 @@ def create_app(
|
|||||||
from .auth.routes import auth_bp
|
from .auth.routes import auth_bp
|
||||||
from .dashboard.routes import dashboard_bp
|
from .dashboard.routes import dashboard_bp
|
||||||
from .hosts.routes import hosts_bp
|
from .hosts.routes import hosts_bp
|
||||||
|
from .ping.routes import ping_bp
|
||||||
from .alerts.routes import alerts_bp
|
from .alerts.routes import alerts_bp
|
||||||
from .ansible.routes import ansible_bp
|
from .ansible.routes import ansible_bp
|
||||||
from .settings.routes import settings_bp
|
from .settings.routes import settings_bp
|
||||||
@@ -93,6 +94,7 @@ def create_app(
|
|||||||
app.register_blueprint(auth_bp)
|
app.register_blueprint(auth_bp)
|
||||||
app.register_blueprint(dashboard_bp)
|
app.register_blueprint(dashboard_bp)
|
||||||
app.register_blueprint(hosts_bp)
|
app.register_blueprint(hosts_bp)
|
||||||
|
app.register_blueprint(ping_bp)
|
||||||
app.register_blueprint(alerts_bp)
|
app.register_blueprint(alerts_bp)
|
||||||
app.register_blueprint(ansible_bp)
|
app.register_blueprint(ansible_bp)
|
||||||
app.register_blueprint(settings_bp)
|
app.register_blueprint(settings_bp)
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ DEFAULTS: dict[str, Any] = {
|
|||||||
"webhook.url": "",
|
"webhook.url": "",
|
||||||
"webhook.template": _DEFAULT_WEBHOOK_TEMPLATE,
|
"webhook.template": _DEFAULT_WEBHOOK_TEMPLATE,
|
||||||
"ansible.sources": [],
|
"ansible.sources": [],
|
||||||
|
"ping.threshold.good_ms": 50,
|
||||||
|
"ping.threshold.warn_ms": 200,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
import logging
|
||||||
|
from quart import Blueprint, current_app, render_template, request, redirect, url_for
|
||||||
|
from sqlalchemy import select, func
|
||||||
|
from fablednetmon.auth.middleware import require_role
|
||||||
|
from fablednetmon.models.users import UserRole
|
||||||
|
from fablednetmon.models.hosts import Host
|
||||||
|
from fablednetmon.models.monitors import PingResult
|
||||||
|
from fablednetmon.core.settings import get_all_settings, set_setting, DEFAULTS
|
||||||
|
|
||||||
|
ping_bp = Blueprint("ping", __name__, url_prefix="/ping")
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def _last_n_pings(db, host_ids: list[str], n: int = 30) -> dict[str, list]:
|
||||||
|
"""Return {host_id: [PingResult]} for last n pings per host, oldest first."""
|
||||||
|
if not host_ids:
|
||||||
|
return {}
|
||||||
|
rn = func.row_number().over(
|
||||||
|
partition_by=PingResult.host_id,
|
||||||
|
order_by=PingResult.probed_at.desc(),
|
||||||
|
).label("rn")
|
||||||
|
subq = (
|
||||||
|
select(PingResult.id, rn)
|
||||||
|
.where(PingResult.host_id.in_(host_ids))
|
||||||
|
.subquery()
|
||||||
|
)
|
||||||
|
pr = await db.execute(
|
||||||
|
select(PingResult)
|
||||||
|
.join(subq, PingResult.id == subq.c.id)
|
||||||
|
.where(subq.c.rn <= n)
|
||||||
|
.order_by(PingResult.host_id, PingResult.probed_at.asc())
|
||||||
|
)
|
||||||
|
result: dict[str, list] = {hid: [] for hid in host_ids}
|
||||||
|
for row in pr.scalars():
|
||||||
|
result[row.host_id].append(row)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
async def _thresholds(db) -> tuple[int, int]:
|
||||||
|
settings = await get_all_settings(db)
|
||||||
|
good = int(settings.get("ping.threshold.good_ms", DEFAULTS["ping.threshold.good_ms"]))
|
||||||
|
warn = int(settings.get("ping.threshold.warn_ms", DEFAULTS["ping.threshold.warn_ms"]))
|
||||||
|
return good, warn
|
||||||
|
|
||||||
|
|
||||||
|
@ping_bp.get("/")
|
||||||
|
@require_role(UserRole.viewer)
|
||||||
|
async def index():
|
||||||
|
async with current_app.db_sessionmaker() as db:
|
||||||
|
result = await db.execute(
|
||||||
|
select(Host).where(Host.ping_enabled.is_(True)).order_by(Host.name)
|
||||||
|
)
|
||||||
|
hosts = result.scalars().all()
|
||||||
|
pings_by_host = await _last_n_pings(db, [h.id for h in hosts])
|
||||||
|
good_ms, warn_ms = await _thresholds(db)
|
||||||
|
|
||||||
|
poll_interval = current_app.config.get("MONITORS_POLL_INTERVAL", 60)
|
||||||
|
return await render_template(
|
||||||
|
"ping/index.html",
|
||||||
|
hosts=hosts,
|
||||||
|
pings_by_host=pings_by_host,
|
||||||
|
good_ms=good_ms,
|
||||||
|
warn_ms=warn_ms,
|
||||||
|
poll_interval=poll_interval,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ping_bp.get("/rows")
|
||||||
|
@require_role(UserRole.viewer)
|
||||||
|
async def rows():
|
||||||
|
"""HTMX fragment — host rows with pills, no page shell."""
|
||||||
|
async with current_app.db_sessionmaker() as db:
|
||||||
|
result = await db.execute(
|
||||||
|
select(Host).where(Host.ping_enabled.is_(True)).order_by(Host.name)
|
||||||
|
)
|
||||||
|
hosts = result.scalars().all()
|
||||||
|
pings_by_host = await _last_n_pings(db, [h.id for h in hosts])
|
||||||
|
good_ms, warn_ms = await _thresholds(db)
|
||||||
|
|
||||||
|
return await render_template(
|
||||||
|
"ping/rows.html",
|
||||||
|
hosts=hosts,
|
||||||
|
pings_by_host=pings_by_host,
|
||||||
|
good_ms=good_ms,
|
||||||
|
warn_ms=warn_ms,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ping_bp.post("/settings")
|
||||||
|
@require_role(UserRole.admin)
|
||||||
|
async def save_settings():
|
||||||
|
form = await request.form
|
||||||
|
try:
|
||||||
|
good = max(1, int(form.get("good_ms", 50)))
|
||||||
|
warn = max(good + 1, int(form.get("warn_ms", 200)))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
good, warn = 50, 200
|
||||||
|
async with current_app.db_sessionmaker() as db:
|
||||||
|
async with db.begin():
|
||||||
|
await set_setting(db, "ping.threshold.good_ms", good)
|
||||||
|
await set_setting(db, "ping.threshold.warn_ms", warn)
|
||||||
|
return redirect(url_for("ping.index"))
|
||||||
@@ -26,6 +26,23 @@
|
|||||||
border-radius: 4px; cursor: pointer; font-size: 0.95rem;
|
border-radius: 4px; cursor: pointer; font-size: 0.95rem;
|
||||||
}
|
}
|
||||||
button[type=submit]:hover, .btn:hover { background: #5050c0; }
|
button[type=submit]:hover, .btn:hover { background: #5050c0; }
|
||||||
|
/* Ping pills */
|
||||||
|
.ping-card { padding: 0.75rem 1.25rem; }
|
||||||
|
.ping-row { display:flex; align-items:center; gap:1rem; padding:0.45rem 0; border-bottom:1px solid #161626; }
|
||||||
|
.ping-row:last-child { border-bottom:none; }
|
||||||
|
.ping-meta { min-width:160px; flex-shrink:0; }
|
||||||
|
.ping-name { font-size:0.9rem; }
|
||||||
|
.ping-addr { display:block; color:#505070; font-size:0.75rem; }
|
||||||
|
.ping-pills { display:flex; gap:2px; flex:1; align-items:center; }
|
||||||
|
.pill { display:inline-block; width:7px; height:22px; border-radius:2px; cursor:default; transition:opacity .1s; }
|
||||||
|
.pill:hover { opacity:.75; }
|
||||||
|
.pill-empty { background:#1a1a28; }
|
||||||
|
.ping-cur { min-width:72px; text-align:right; font-size:0.85rem; font-variant-numeric:tabular-nums; }
|
||||||
|
.ping-cur-good { color:#22aa44; }
|
||||||
|
.ping-cur-warn { color:#ccaa20; }
|
||||||
|
.ping-cur-bad { color:#dd6020; }
|
||||||
|
.ping-cur-down { color:#cc2020; font-weight:bold; }
|
||||||
|
.ping-cur-nd { color:#404060; }
|
||||||
</style>
|
</style>
|
||||||
{% block head %}{% endblock %}
|
{% block head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
@@ -35,6 +52,7 @@
|
|||||||
<span class="brand">FabledNetMon</span>
|
<span class="brand">FabledNetMon</span>
|
||||||
<a href="/">Dashboard</a>
|
<a href="/">Dashboard</a>
|
||||||
<a href="/hosts/">Hosts</a>
|
<a href="/hosts/">Hosts</a>
|
||||||
|
<a href="/ping/">Ping</a>
|
||||||
<a href="/alerts/">Alerts</a>
|
<a href="/alerts/">Alerts</a>
|
||||||
<a href="/ansible/">Ansible</a>
|
<a href="/ansible/">Ansible</a>
|
||||||
{% if session.user_role == 'admin' %}
|
{% if session.user_role == 'admin' %}
|
||||||
|
|||||||
@@ -2,60 +2,55 @@
|
|||||||
{% block title %}Dashboard — FabledNetMon{% endblock %}
|
{% block title %}Dashboard — FabledNetMon{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1 style="margin-bottom:1.5rem;color:#c0c0ff;">Dashboard</h1>
|
<h1 style="margin-bottom:1.5rem;color:#c0c0ff;">Dashboard</h1>
|
||||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:1rem;">
|
|
||||||
|
|
||||||
<div class="card">
|
{# ── Summary row ──────────────────────────────────────────────────────────── #}
|
||||||
<h3 style="color:#8080ff;margin-bottom:0.75rem;font-size:0.9rem;text-transform:uppercase;letter-spacing:0.05em;">Ping</h3>
|
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:1rem;margin-bottom:1.5rem;">
|
||||||
{% if ping_up + ping_down == 0 %}
|
<div class="card">
|
||||||
<p style="color:#606080;font-size:0.9rem;"><a href="/hosts/" style="color:#a0a0ff;">Add hosts</a> to begin monitoring.</p>
|
<h3 style="color:#8080ff;margin-bottom:0.5rem;font-size:0.85rem;text-transform:uppercase;letter-spacing:.05em;">Hosts</h3>
|
||||||
{% else %}
|
<span style="font-size:1.8rem;font-weight:bold;color:#c0c0e0;">{{ total_hosts }}</span>
|
||||||
<div style="display:flex;gap:1.5rem;align-items:baseline;">
|
<span style="color:#505070;font-size:0.8rem;margin-left:0.3rem;">configured</span>
|
||||||
<div>
|
<p style="margin-top:0.4rem;"><a href="/hosts/" style="color:#6060a0;font-size:0.8rem;">Manage →</a></p>
|
||||||
<span style="font-size:2rem;font-weight:bold;color:#40c040;">{{ ping_up }}</span>
|
</div>
|
||||||
<span style="color:#606080;font-size:0.85rem;margin-left:0.25rem;">up</span>
|
<div class="card">
|
||||||
</div>
|
<h3 style="color:#8080ff;margin-bottom:0.5rem;font-size:0.85rem;text-transform:uppercase;letter-spacing:.05em;">Ping</h3>
|
||||||
{% if ping_down > 0 %}
|
{% if ping_up + ping_down == 0 %}
|
||||||
<div>
|
<p style="color:#505070;font-size:0.85rem;"><a href="/hosts/" style="color:#a0a0ff;">Enable ping</a> on a host.</p>
|
||||||
<span style="font-size:2rem;font-weight:bold;color:#c04040;">{{ ping_down }}</span>
|
{% else %}
|
||||||
<span style="color:#606080;font-size:0.85rem;margin-left:0.25rem;">down</span>
|
<div style="display:flex;gap:1rem;align-items:baseline;">
|
||||||
</div>
|
<div><span style="font-size:1.8rem;font-weight:bold;color:#40c040;">{{ ping_up }}</span><span style="color:#505070;font-size:0.8rem;margin-left:0.2rem;">up</span></div>
|
||||||
{% endif %}
|
{% if ping_down %}<div><span style="font-size:1.8rem;font-weight:bold;color:#c04040;">{{ ping_down }}</span><span style="color:#505070;font-size:0.8rem;margin-left:0.2rem;">down</span></div>{% endif %}
|
||||||
</div>
|
|
||||||
<p style="color:#404060;font-size:0.8rem;margin-top:0.5rem;">every {{ poll_interval }}s — <a href="/hosts/" style="color:#6060a0;">view all</a></p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="card">
|
</div>
|
||||||
<h3 style="color:#8080ff;margin-bottom:0.75rem;font-size:0.9rem;text-transform:uppercase;letter-spacing:0.05em;">DNS</h3>
|
<div class="card">
|
||||||
{% if dns_ok + dns_fail == 0 %}
|
<h3 style="color:#8080ff;margin-bottom:0.5rem;font-size:0.85rem;text-transform:uppercase;letter-spacing:.05em;">DNS</h3>
|
||||||
<p style="color:#606080;font-size:0.9rem;">Enable DNS monitoring on a <a href="/hosts/" style="color:#a0a0ff;">host</a>.</p>
|
{% if dns_ok + dns_fail == 0 %}
|
||||||
{% else %}
|
<p style="color:#505070;font-size:0.85rem;"><a href="/hosts/" style="color:#a0a0ff;">Enable DNS</a> on a host.</p>
|
||||||
<div style="display:flex;gap:1.5rem;align-items:baseline;">
|
{% else %}
|
||||||
<div>
|
<div style="display:flex;gap:1rem;align-items:baseline;">
|
||||||
<span style="font-size:2rem;font-weight:bold;color:#40c040;">{{ dns_ok }}</span>
|
<div><span style="font-size:1.8rem;font-weight:bold;color:#40c040;">{{ dns_ok }}</span><span style="color:#505070;font-size:0.8rem;margin-left:0.2rem;">ok</span></div>
|
||||||
<span style="color:#606080;font-size:0.85rem;margin-left:0.25rem;">ok</span>
|
{% if dns_fail %}<div><span style="font-size:1.8rem;font-weight:bold;color:#c04040;">{{ dns_fail }}</span><span style="color:#505070;font-size:0.8rem;margin-left:0.2rem;">failed</span></div>{% endif %}
|
||||||
</div>
|
|
||||||
{% if dns_fail > 0 %}
|
|
||||||
<div>
|
|
||||||
<span style="font-size:2rem;font-weight:bold;color:#c04040;">{{ dns_fail }}</span>
|
|
||||||
<span style="color:#606080;font-size:0.85rem;margin-left:0.25rem;">failed</span>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<h3 style="color:#8080ff;margin-bottom:0.75rem;font-size:0.9rem;text-transform:uppercase;letter-spacing:0.05em;">Hosts</h3>
|
|
||||||
<span style="font-size:2rem;font-weight:bold;color:#c0c0e0;">{{ total_hosts }}</span>
|
|
||||||
<span style="color:#606080;font-size:0.85rem;margin-left:0.25rem;">configured</span>
|
|
||||||
<p style="margin-top:0.5rem;"><a href="/hosts/" style="color:#6060a0;font-size:0.85rem;">Manage →</a></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<h3 style="color:#8080ff;margin-bottom:0.75rem;font-size:0.9rem;text-transform:uppercase;letter-spacing:0.05em;">Alerts</h3>
|
|
||||||
<p style="color:#606080;font-size:0.9rem;"><a href="/alerts/" style="color:#a0a0ff;">Configure alert rules</a> against any metric.</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h3 style="color:#8080ff;margin-bottom:0.5rem;font-size:0.85rem;text-transform:uppercase;letter-spacing:.05em;">Alerts</h3>
|
||||||
|
<p style="color:#505070;font-size:0.85rem;"><a href="/alerts/" style="color:#a0a0ff;">Configure rules →</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# ── Live ping widget ─────────────────────────────────────────────────────── #}
|
||||||
|
<div class="card ping-card">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.75rem;">
|
||||||
|
<h2 style="color:#8080ff;font-size:0.9rem;text-transform:uppercase;letter-spacing:.05em;">
|
||||||
|
Ping — last 30 probes
|
||||||
|
</h2>
|
||||||
|
<a href="/ping/" style="color:#6060a0;font-size:0.8rem;">Details & settings →</a>
|
||||||
|
</div>
|
||||||
|
<div id="ping-widget"
|
||||||
|
hx-get="/ping/rows"
|
||||||
|
hx-trigger="load, every {{ poll_interval }}s"
|
||||||
|
hx-swap="innerHTML">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Ping Monitor — FabledNetMon{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1.5rem;">
|
||||||
|
<h1 style="color:#c0c0ff;">Ping Monitor</h1>
|
||||||
|
<span style="color:#505070;font-size:0.85rem;">polling every {{ poll_interval }}s</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# ── Threshold settings ─────────────────────────────────────────────────── #}
|
||||||
|
<div class="card" style="margin-bottom:1.25rem;">
|
||||||
|
<h2 style="color:#8080ff;font-size:0.9rem;text-transform:uppercase;letter-spacing:0.05em;margin-bottom:1rem;">Latency Thresholds</h2>
|
||||||
|
<form method="post" action="/ping/settings"
|
||||||
|
style="display:flex;flex-wrap:wrap;gap:1rem;align-items:flex-end;">
|
||||||
|
<div class="form-group" style="margin:0;">
|
||||||
|
<label style="font-size:0.8rem;">
|
||||||
|
<span style="display:inline-block;width:10px;height:10px;border-radius:2px;background:#1a6632;vertical-align:middle;margin-right:4px;"></span>
|
||||||
|
Good below (ms)
|
||||||
|
</label>
|
||||||
|
<input type="number" name="good_ms" value="{{ good_ms }}" min="1" max="9999"
|
||||||
|
style="width:100px;padding:0.4rem 0.6rem;background:#0f0f1e;border:1px solid #3a3a5a;border-radius:4px;color:#e0e0e0;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin:0;">
|
||||||
|
<label style="font-size:0.8rem;">
|
||||||
|
<span style="display:inline-block;width:10px;height:10px;border-radius:2px;background:#6b5c00;vertical-align:middle;margin-right:4px;"></span>
|
||||||
|
Warn above (ms)
|
||||||
|
</label>
|
||||||
|
<input type="number" name="warn_ms" value="{{ warn_ms }}" min="1" max="9999"
|
||||||
|
style="width:100px;padding:0.4rem 0.6rem;background:#0f0f1e;border:1px solid #3a3a5a;border-radius:4px;color:#e0e0e0;">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn" style="padding:0.4rem 1rem;">Save</button>
|
||||||
|
<span style="color:#505070;font-size:0.8rem;align-self:center;">
|
||||||
|
Above warn = <span style="color:#c06020;">■</span> orange | No response = <span style="color:#c03030;">■</span> red
|
||||||
|
</span>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# ── Live host rows ─────────────────────────────────────────────────────── #}
|
||||||
|
<div class="card ping-card">
|
||||||
|
<div id="ping-rows"
|
||||||
|
hx-get="/ping/rows"
|
||||||
|
hx-trigger="every {{ poll_interval }}s"
|
||||||
|
hx-swap="innerHTML">
|
||||||
|
{% include "ping/rows.html" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
{# HTMX fragment — included in both /ping/ page and dashboard widget #}
|
||||||
|
{% macro pill_bg(p) %}
|
||||||
|
{%- if p is none or p.status.value == 'down' or p.response_time_ms is none -%}
|
||||||
|
#6a1515
|
||||||
|
{%- elif p.response_time_ms <= good_ms -%}
|
||||||
|
#1a6632
|
||||||
|
{%- elif p.response_time_ms <= warn_ms -%}
|
||||||
|
#6b5c00
|
||||||
|
{%- else -%}
|
||||||
|
#7a3800
|
||||||
|
{%- endif -%}
|
||||||
|
{% endmacro %}
|
||||||
|
{% macro pill_title(p) %}
|
||||||
|
{%- if p is none -%}No data
|
||||||
|
{%- elif p.status.value == 'down' -%}Down — {{ p.probed_at.strftime('%H:%M:%S') }} UTC
|
||||||
|
{%- else -%}{{ "%.1f"|format(p.response_time_ms) }} ms — {{ p.probed_at.strftime('%H:%M:%S') }} UTC
|
||||||
|
{%- endif -%}
|
||||||
|
{% endmacro %}
|
||||||
|
{% if hosts %}
|
||||||
|
{% for host in hosts %}
|
||||||
|
{% set host_pings = pings_by_host.get(host.id, []) %}
|
||||||
|
{% set last = host_pings[-1] if host_pings else none %}
|
||||||
|
<div class="ping-row">
|
||||||
|
<div class="ping-meta">
|
||||||
|
<span class="ping-name">{{ host.name }}</span>
|
||||||
|
<span class="ping-addr">{{ host.address }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="ping-pills">
|
||||||
|
{% for _ in range([30 - host_pings|length, 0]|max) %}
|
||||||
|
<span class="pill pill-empty" title="No data"></span>
|
||||||
|
{% endfor %}
|
||||||
|
{% for p in host_pings %}
|
||||||
|
<span class="pill" style="background:{{ pill_bg(p) }};" title="{{ pill_title(p) }}"></span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="ping-cur">
|
||||||
|
{% if last is none %}
|
||||||
|
<span class="ping-cur-nd">—</span>
|
||||||
|
{% elif last.status.value == 'down' or last.response_time_ms is none %}
|
||||||
|
<span class="ping-cur-down">DOWN</span>
|
||||||
|
{% elif last.response_time_ms <= good_ms %}
|
||||||
|
<span class="ping-cur-good">{{ "%.1f"|format(last.response_time_ms) }} ms</span>
|
||||||
|
{% elif last.response_time_ms <= warn_ms %}
|
||||||
|
<span class="ping-cur-warn">{{ "%.1f"|format(last.response_time_ms) }} ms</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="ping-cur-bad">{{ "%.1f"|format(last.response_time_ms) }} ms</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<p style="color:#505070;font-size:0.85rem;padding:0.5rem 0;">No ping-enabled hosts. <a href="/hosts/" style="color:#7070c0;">Add hosts →</a></p>
|
||||||
|
{% endif %}
|
||||||
Reference in New Issue
Block a user