feat: initial release — traefik, unifi, ups plugins
First-party plugins for Fabled Scryer (https://github.com/bvandeusen/fabledscryer). traefik: Prometheus metrics scraping, stats history, access log parsing (GeoIP optional) unifi: UniFi controller integration — WAN health, devices, clients, DPI, events ups: UPS monitoring via NUT (Network UPS Tools), Ansible shutdown automation Each plugin follows the Fabled Scryer plugin contract: setup(app), get_scheduled_tasks(), get_blueprint() index.yaml lists all three plugins in the catalog format. Populate checksum_sha256 entries once release zips are published via GitHub Releases. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
{# plugins/unifi/templates/unifi/dpi.html #}
|
||||
{% if not snapshot %}
|
||||
<div class="card">
|
||||
<p style="color:var(--text-muted);">No DPI data yet. DPI must be enabled on the UniFi controller.</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div style="column-width:340px;column-count:3;column-gap:1rem;">
|
||||
|
||||
{# Category breakdown #}
|
||||
{% if categories %}
|
||||
<div class="card" style="break-inside:avoid;margin-bottom:1rem;">
|
||||
<div class="section-title" style="margin-bottom:0.75rem;">
|
||||
Traffic by Category
|
||||
<span style="float:right;font-weight:normal;font-size:0.78rem;color:var(--text-dim);">{{ snapshot.scraped_at.strftime("%H:%M") }}</span>
|
||||
</div>
|
||||
{% set total_bytes = categories | sum(attribute='rx_bytes') + categories | sum(attribute='tx_bytes') %}
|
||||
<table class="table" style="width:100%;font-size:0.82rem;">
|
||||
{% for cat in categories[:20] %}
|
||||
{% set bytes = cat.rx_bytes + cat.tx_bytes %}
|
||||
{% set pct = (bytes / total_bytes * 100) if total_bytes else 0 %}
|
||||
<tr>
|
||||
<td style="padding:0.25rem 0.5rem 0.25rem 0;">{{ cat.cat_name or cat.cat_id }}</td>
|
||||
<td style="padding:0.25rem 0.3rem;text-align:right;color:var(--text-muted);font-size:0.78rem;white-space:nowrap;">
|
||||
{% if bytes >= 1073741824 %}{{ "%.1f"|format(bytes / 1073741824) }} GB
|
||||
{% elif bytes >= 1048576 %}{{ "%.1f"|format(bytes / 1048576) }} MB
|
||||
{% elif bytes >= 1024 %}{{ "%.0f"|format(bytes / 1024) }} KB
|
||||
{% else %}{{ bytes }} B{% endif %}
|
||||
</td>
|
||||
<td style="padding:0.25rem 0;width:4rem;">
|
||||
<div style="background:var(--border);border-radius:2px;height:6px;overflow:hidden;">
|
||||
<div style="background:var(--accent);height:100%;width:{{ "%.1f"|format(pct) }}%;"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Top clients by DPI #}
|
||||
{% if top_clients %}
|
||||
<div class="card" style="break-inside:avoid;margin-bottom:1rem;">
|
||||
<div class="section-title" style="margin-bottom:0.75rem;">Top Clients by Traffic</div>
|
||||
<table class="table" style="width:100%;font-size:0.82rem;">
|
||||
{% for c in top_clients %}
|
||||
<tr>
|
||||
<td style="padding:0.25rem 0.5rem 0.25rem 0;word-break:break-all;">
|
||||
{{ c.hostname or c.mac }}
|
||||
<span style="color:var(--text-dim);font-size:0.75rem;font-family:monospace;margin-left:0.3rem;">{{ c.mac }}</span>
|
||||
</td>
|
||||
<td style="padding:0.25rem 0;text-align:right;font-size:0.78rem;color:var(--text-muted);white-space:nowrap;">
|
||||
{% set bytes = c.bytes %}
|
||||
{% if bytes >= 1073741824 %}{{ "%.1f"|format(bytes / 1073741824) }} GB
|
||||
{% elif bytes >= 1048576 %}{{ "%.1f"|format(bytes / 1048576) }} MB
|
||||
{% elif bytes >= 1024 %}{{ "%.0f"|format(bytes / 1024) }} KB
|
||||
{% else %}{{ bytes }} B{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,52 @@
|
||||
{# plugins/unifi/templates/unifi/events.html #}
|
||||
{% if not events %}
|
||||
<div class="card">
|
||||
<p style="color:var(--text-muted);">No events in the last {{ range_key }}.</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card">
|
||||
<div class="section-title" style="margin-bottom:0.75rem;">
|
||||
Site Events
|
||||
<span style="float:right;font-weight:normal;font-size:0.8rem;color:var(--text-dim);">{{ events|length }} in {{ range_key }}</span>
|
||||
</div>
|
||||
<table class="table" style="width:100%;font-size:0.82rem;">
|
||||
<colgroup>
|
||||
<col style="width:12rem;">
|
||||
<col style="width:5rem;">
|
||||
<col style="width:5rem;">
|
||||
<col>
|
||||
</colgroup>
|
||||
<tr style="color:var(--text-muted);font-size:0.78rem;">
|
||||
<th style="text-align:left;font-weight:normal;padding:0 0.5rem 0.4rem 0;">Time</th>
|
||||
<th style="text-align:left;font-weight:normal;padding:0 0.5rem 0.4rem;">Category</th>
|
||||
<th style="text-align:left;font-weight:normal;padding:0 0.5rem 0.4rem;">Key</th>
|
||||
<th style="text-align:left;font-weight:normal;padding:0 0 0.4rem;">Message</th>
|
||||
</tr>
|
||||
{% for ev in events %}
|
||||
<tr style="{% if loop.index is divisibleby 2 %}background:var(--row-alt,rgba(255,255,255,0.02));{% endif %}">
|
||||
<td style="padding:0.25rem 0.5rem 0.25rem 0;color:var(--text-dim);font-size:0.77rem;white-space:nowrap;">
|
||||
{{ ev.event_time.strftime("%Y-%m-%d %H:%M:%S") }}
|
||||
</td>
|
||||
<td style="padding:0.25rem 0.5rem;">
|
||||
<span style="font-size:0.75rem;padding:0.1rem 0.4rem;border-radius:3px;background:
|
||||
{% if ev.category == 'wlan' %}rgba(96,96,192,0.25)
|
||||
{% elif ev.category == 'wan' %}rgba(96,160,96,0.25)
|
||||
{% elif ev.category == 'ids' %}rgba(192,64,64,0.25)
|
||||
{% else %}rgba(128,128,128,0.15){% endif %};">
|
||||
{{ ev.category }}
|
||||
</span>
|
||||
</td>
|
||||
<td style="padding:0.25rem 0.5rem;color:var(--text-dim);font-family:monospace;font-size:0.75rem;">
|
||||
{{ ev.event_key.replace("EVT_", "") if ev.event_key else "" }}
|
||||
</td>
|
||||
<td style="padding:0.25rem 0;color:var(--text);">
|
||||
{{ ev.message }}
|
||||
{% if ev.source_mac %}
|
||||
<span style="color:var(--text-dim);font-size:0.75rem;font-family:monospace;margin-left:0.4rem;">{{ ev.source_mac }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,63 @@
|
||||
{# plugins/unifi/templates/unifi/index.html #}
|
||||
{% extends "base.html" %}
|
||||
{% block title %}UniFi — Fabled Scryer{% endblock %}
|
||||
{% block content %}
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:0.75rem;margin-bottom:1.25rem;">
|
||||
<div style="display:flex;align-items:baseline;gap:1rem;">
|
||||
<h1 class="page-title" style="margin-bottom:0;">UniFi Network</h1>
|
||||
<span style="color:var(--text-muted);font-size:0.85rem;">polling every {{ poll_interval }}s</span>
|
||||
</div>
|
||||
{% include "_time_range.html" %}
|
||||
</div>
|
||||
|
||||
{# Tab nav #}
|
||||
<div style="display:flex;gap:0.25rem;margin-bottom:1.25rem;border-bottom:1px solid var(--border);padding-bottom:0;">
|
||||
{% set tabs = [
|
||||
("overview", "Overview", "/plugins/unifi/rows"),
|
||||
("events", "Events", "/plugins/unifi/events"),
|
||||
("dpi", "Traffic", "/plugins/unifi/dpi"),
|
||||
("security", "Security", "/plugins/unifi/security"),
|
||||
("inventory", "Inventory", "/plugins/unifi/inventory"),
|
||||
] %}
|
||||
{% for tab_id, label, url in tabs %}
|
||||
<button
|
||||
class="tab-btn{% if loop.first %} tab-active{% endif %}"
|
||||
data-tab="{{ tab_id }}"
|
||||
data-url="{{ url }}"
|
||||
onclick="setUnifiTab(this)"
|
||||
style="background:none;border:none;border-bottom:2px solid {% if loop.first %}var(--accent){% else %}transparent{% endif %};padding:0.4rem 0.9rem;cursor:pointer;font-size:0.88rem;color:{% if loop.first %}var(--text){% else %}var(--text-muted){% endif %};margin-bottom:-1px;">
|
||||
{{ label }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div id="unifi-content"
|
||||
hx-get="/plugins/unifi/rows"
|
||||
hx-trigger="load, every {{ poll_interval }}s, rangeChange from:body"
|
||||
hx-include="#time-range"
|
||||
hx-swap="innerHTML">
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function setUnifiTab(btn) {
|
||||
document.querySelectorAll('.tab-btn').forEach(function(b) {
|
||||
b.style.borderBottomColor = 'transparent';
|
||||
b.style.color = 'var(--text-muted)';
|
||||
});
|
||||
btn.style.borderBottomColor = 'var(--accent)';
|
||||
btn.style.color = 'var(--text)';
|
||||
|
||||
var el = document.getElementById('unifi-content');
|
||||
el.setAttribute('hx-get', btn.dataset.url);
|
||||
// Remove auto-poll on non-overview tabs (inventory/security don't change rapidly)
|
||||
var isLive = btn.dataset.tab === 'overview' || btn.dataset.tab === 'events';
|
||||
el.setAttribute('hx-trigger',
|
||||
isLive
|
||||
? 'tabActivated, every {{ poll_interval }}s, rangeChange from:body'
|
||||
: 'tabActivated, rangeChange from:body'
|
||||
);
|
||||
htmx.process(el);
|
||||
htmx.trigger(el, 'tabActivated');
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,146 @@
|
||||
{# plugins/unifi/templates/unifi/inventory.html #}
|
||||
<div style="column-width:360px;column-count:3;column-gap:1rem;">
|
||||
|
||||
{# Networks / VLANs #}
|
||||
{% if networks %}
|
||||
<div class="card" style="break-inside:avoid;margin-bottom:1rem;">
|
||||
<div class="section-title" style="margin-bottom:0.75rem;">Networks / VLANs</div>
|
||||
<table class="table" style="width:100%;font-size:0.82rem;">
|
||||
<tr style="color:var(--text-muted);font-size:0.78rem;">
|
||||
<th style="text-align:left;font-weight:normal;padding:0 0.5rem 0.3rem 0;">Name</th>
|
||||
<th style="text-align:right;font-weight:normal;padding:0 0.5rem 0.3rem;">VLAN</th>
|
||||
<th style="text-align:left;font-weight:normal;padding:0 0 0.3rem;">Subnet</th>
|
||||
</tr>
|
||||
{% for net in networks %}
|
||||
<tr>
|
||||
<td style="padding:0.25rem 0.5rem 0.25rem 0;">
|
||||
{{ net.name }}
|
||||
{% if net.is_guest %}<span style="font-size:0.72rem;color:var(--text-dim);"> guest</span>{% endif %}
|
||||
</td>
|
||||
<td style="padding:0.25rem 0.5rem;text-align:right;color:var(--text-muted);font-family:monospace;font-size:0.78rem;">
|
||||
{{ net.vlan or "—" }}
|
||||
</td>
|
||||
<td style="padding:0.25rem 0;color:var(--text-dim);font-family:monospace;font-size:0.78rem;">
|
||||
{{ net.ip_subnet or "—" }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Port forwards #}
|
||||
{% if port_forwards %}
|
||||
<div class="card" style="break-inside:avoid;margin-bottom:1rem;">
|
||||
<div class="section-title" style="margin-bottom:0.75rem;">Port Forwards</div>
|
||||
<table class="table" style="width:100%;font-size:0.82rem;">
|
||||
{% for pf in port_forwards %}
|
||||
<tr>
|
||||
<td style="padding:0.25rem 0.5rem 0.25rem 0;">
|
||||
<span style="{% if not pf.enabled %}color:var(--text-dim);{% endif %}">
|
||||
{{ pf.name or "—" }}
|
||||
</span>
|
||||
{% if not pf.enabled %}<span style="font-size:0.72rem;color:var(--text-dim);"> disabled</span>{% endif %}
|
||||
</td>
|
||||
<td style="padding:0.25rem 0;font-family:monospace;font-size:0.78rem;color:var(--text-muted);white-space:nowrap;">
|
||||
{{ pf.proto }}:{{ pf.dst_port }} → {{ pf.fwd_ip }}:{{ pf.fwd_port }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Firewall rules, grouped by ruleset #}
|
||||
{% if fw_rules %}
|
||||
{% set ns = namespace(current_rs=None) %}
|
||||
<div class="card" style="break-inside:avoid;margin-bottom:1rem;">
|
||||
<div class="section-title" style="margin-bottom:0.75rem;">Firewall Rules</div>
|
||||
<table class="table" style="width:100%;font-size:0.82rem;">
|
||||
{% for rule in fw_rules %}
|
||||
{% if rule.ruleset != ns.current_rs %}
|
||||
{% set ns.current_rs = rule.ruleset %}
|
||||
<tr>
|
||||
<td colspan="3" style="padding:0.4rem 0 0.2rem;color:var(--text-muted);font-size:0.75rem;font-weight:600;letter-spacing:0.04em;text-transform:uppercase;">
|
||||
{{ rule.ruleset }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td style="padding:0.2rem 0.5rem 0.2rem 0;">
|
||||
<span style="{% if not rule.enabled %}color:var(--text-dim);{% endif %}">
|
||||
{{ rule.name }}
|
||||
</span>
|
||||
</td>
|
||||
<td style="padding:0.2rem 0.3rem;white-space:nowrap;">
|
||||
<span style="font-size:0.75rem;padding:0.1rem 0.35rem;border-radius:3px;background:
|
||||
{% if rule.action == 'accept' %}rgba(64,160,64,0.2)
|
||||
{% elif rule.action == 'drop' %}rgba(192,64,64,0.2)
|
||||
{% else %}rgba(192,128,64,0.2){% endif %};">
|
||||
{{ rule.action }}
|
||||
</span>
|
||||
</td>
|
||||
<td style="padding:0.2rem 0;color:var(--text-dim);font-size:0.76rem;">
|
||||
{% if rule.protocol %}{{ rule.protocol }}{% endif %}
|
||||
{% if rule.src_address %} src:{{ rule.src_address }}{% endif %}
|
||||
{% if rule.dst_address %} dst:{{ rule.dst_address }}{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Known clients #}
|
||||
{% if known_clients %}
|
||||
<div class="card" style="break-inside:avoid;margin-bottom:1rem;">
|
||||
<div class="section-title" style="margin-bottom:0.75rem;">
|
||||
Known Clients
|
||||
<span style="float:right;font-weight:normal;font-size:0.78rem;color:var(--text-dim);">{{ known_clients|length }} total</span>
|
||||
</div>
|
||||
<table class="table" style="width:100%;font-size:0.82rem;">
|
||||
<tr style="color:var(--text-muted);font-size:0.78rem;">
|
||||
<th style="text-align:left;font-weight:normal;padding:0 0.5rem 0.3rem 0;">Name / MAC</th>
|
||||
<th style="text-align:left;font-weight:normal;padding:0 0.5rem 0.3rem;">IP</th>
|
||||
<th style="text-align:right;font-weight:normal;padding:0 0 0.3rem;">Last Seen</th>
|
||||
</tr>
|
||||
{% for kc in known_clients[:50] %}
|
||||
<tr>
|
||||
<td style="padding:0.2rem 0.5rem 0.2rem 0;">
|
||||
<div style="{% if kc.is_blocked %}color:var(--red);{% endif %}">
|
||||
{{ kc.name or kc.hostname or kc.mac }}
|
||||
{% if kc.is_blocked %}<span style="font-size:0.72rem;"> blocked</span>{% endif %}
|
||||
</div>
|
||||
{% if kc.name or kc.hostname %}
|
||||
<div style="color:var(--text-dim);font-size:0.74rem;font-family:monospace;">{{ kc.mac }}</div>
|
||||
{% endif %}
|
||||
{% if kc.mac_vendor %}
|
||||
<div style="color:var(--text-dim);font-size:0.73rem;">{{ kc.mac_vendor }}</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="padding:0.2rem 0.3rem;color:var(--text-muted);font-family:monospace;font-size:0.78rem;">
|
||||
{{ kc.ip or "—" }}
|
||||
</td>
|
||||
<td style="padding:0.2rem 0;text-align:right;color:var(--text-dim);font-size:0.75rem;white-space:nowrap;">
|
||||
{% if kc.last_seen %}{{ kc.last_seen.strftime("%m-%d %H:%M") }}{% else %}—{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if known_clients|length > 50 %}
|
||||
<tr>
|
||||
<td colspan="3" style="padding:0.4rem 0;color:var(--text-dim);font-size:0.78rem;text-align:center;">
|
||||
+{{ known_clients|length - 50 }} more
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if not networks and not port_forwards and not fw_rules and not known_clients %}
|
||||
<div class="card">
|
||||
<p style="color:var(--text-muted);">No inventory data yet. This populates on the next poll cycle.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,203 @@
|
||||
{# plugins/unifi/templates/unifi/rows.html #}
|
||||
|
||||
{# ── WAN health bar ───────────────────────────────────────────────────────── #}
|
||||
{% if latest_wan %}
|
||||
<div style="display:flex;flex-wrap:wrap;gap:1.5rem;margin-bottom:1.25rem;padding:0.75rem 1rem;background:var(--card-bg);border:1px solid var(--border);border-radius:6px;font-size:0.85rem;font-variant-numeric:tabular-nums;align-items:center;">
|
||||
<span>
|
||||
<span style="color:var(--text-muted);">WAN</span>
|
||||
<span style="margin-left:0.4rem;font-weight:600;color:{% if latest_wan.is_up %}var(--green){% else %}var(--red){% endif %};">
|
||||
{% if latest_wan.is_up %}✓ Up{% else %}✗ Down{% endif %}
|
||||
</span>
|
||||
{% if latest_wan.wan_ip %}
|
||||
<span style="color:var(--text-dim);margin-left:0.4rem;font-family:monospace;font-size:0.8rem;">{{ latest_wan.wan_ip }}</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
|
||||
{% if latest_wan.latency_ms is not none %}
|
||||
<span>
|
||||
<span style="color:var(--text-muted);">Latency</span>
|
||||
<span style="margin-left:0.4rem;
|
||||
{% if latest_wan.latency_ms > 100 %}color:var(--red){% elif latest_wan.latency_ms > 50 %}color:var(--yellow){% else %}color:var(--green){% endif %};">
|
||||
{{ "%.1f"|format(latest_wan.latency_ms) }}ms
|
||||
</span>
|
||||
<span style="margin-left:0.3rem;">{{ sparkline_latency | safe }}</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
{% if latest_wan.rx_bytes_rate is not none %}
|
||||
<span>
|
||||
<span style="color:var(--text-muted);">↓ RX</span>
|
||||
<span style="margin-left:0.3rem;">
|
||||
{% set bps = latest_wan.rx_bytes_rate %}
|
||||
{% if bps >= 1048576 %}{{ "%.1f"|format(bps / 1048576) }} MB/s
|
||||
{% elif bps >= 1024 %}{{ "%.0f"|format(bps / 1024) }} KB/s
|
||||
{% else %}{{ "%.0f"|format(bps) }} B/s{% endif %}
|
||||
</span>
|
||||
<span style="margin-left:0.3rem;">{{ sparkline_rx | safe }}</span>
|
||||
</span>
|
||||
<span>
|
||||
<span style="color:var(--text-muted);">↑ TX</span>
|
||||
<span style="margin-left:0.3rem;">
|
||||
{% set bps = latest_wan.tx_bytes_rate %}
|
||||
{% if bps >= 1048576 %}{{ "%.1f"|format(bps / 1048576) }} MB/s
|
||||
{% elif bps >= 1024 %}{{ "%.0f"|format(bps / 1024) }} KB/s
|
||||
{% else %}{{ "%.0f"|format(bps) }} B/s{% endif %}
|
||||
</span>
|
||||
<span style="margin-left:0.3rem;">{{ sparkline_tx | safe }}</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
{% if latest_wan.uptime_seconds is not none %}
|
||||
<span style="color:var(--text-dim);font-size:0.8rem;">
|
||||
up {{ (latest_wan.uptime_seconds // 3600) }}h {{ ((latest_wan.uptime_seconds % 3600) // 60) }}m
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# ── Content columns ──────────────────────────────────────────────────────── #}
|
||||
<div style="column-width:340px;column-count:3;column-gap:1rem;">
|
||||
|
||||
{# Client summary card #}
|
||||
{% if latest_clients %}
|
||||
<div class="card" style="break-inside:avoid;margin-bottom:1rem;">
|
||||
<div class="section-title" style="margin-bottom:0.75rem;">
|
||||
Clients
|
||||
<span style="float:right;font-weight:normal;color:var(--text-muted);font-size:0.82rem;">{{ sparkline_clients | safe }}</span>
|
||||
</div>
|
||||
<div style="display:flex;gap:1.5rem;font-size:0.9rem;margin-bottom:0.75rem;font-variant-numeric:tabular-nums;">
|
||||
<span>
|
||||
<span style="font-size:1.4rem;font-weight:700;color:var(--text);">{{ latest_clients.total_clients }}</span>
|
||||
<span style="color:var(--text-muted);font-size:0.8rem;margin-left:0.2rem;">total</span>
|
||||
</span>
|
||||
<span style="color:var(--text-muted);font-size:0.85rem;align-self:flex-end;">
|
||||
{{ latest_clients.wireless_clients }}↗ wireless
|
||||
{{ latest_clients.wired_clients }}↔ wired
|
||||
{% if latest_clients.guest_clients %} {{ latest_clients.guest_clients }} guest{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Infrastructure devices card #}
|
||||
{% if devices %}
|
||||
<div class="card" style="break-inside:avoid;margin-bottom:1rem;">
|
||||
<div class="section-title" style="margin-bottom:0.75rem;">Infrastructure Devices</div>
|
||||
<table class="table" style="font-size:0.83rem;width:100%;">
|
||||
{% for d in devices %}
|
||||
<tr>
|
||||
<td style="padding:0.2rem 0.5rem 0.2rem 0;">
|
||||
<span style="color:{% if d.state == 1 %}var(--green){% else %}var(--red){% endif %};">●</span>
|
||||
<span style="margin-left:0.3rem;">{{ d.name or d.mac }}</span>
|
||||
</td>
|
||||
<td style="padding:0.2rem 0.3rem;color:var(--text-muted);font-size:0.78rem;">{{ d.model or d.device_type or "" }}</td>
|
||||
<td style="text-align:right;padding:0.2rem 0;color:var(--text-dim);font-size:0.78rem;font-family:monospace;">{{ d.ip or "" }}</td>
|
||||
</tr>
|
||||
{% if d.state == 1 and d.uptime_seconds %}
|
||||
<tr>
|
||||
<td colspan="3" style="padding:0 0 0.3rem 1.2rem;color:var(--text-dim);font-size:0.75rem;">
|
||||
up {{ (d.uptime_seconds // 86400) }}d {{ ((d.uptime_seconds % 86400) // 3600) }}h
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Top talkers card #}
|
||||
{% if top_clients %}
|
||||
<div class="card" style="break-inside:avoid;margin-bottom:1rem;">
|
||||
<div class="section-title" style="margin-bottom:0.75rem;">Top Talkers</div>
|
||||
<table class="table" style="font-size:0.82rem;width:100%;">
|
||||
{% for c in top_clients %}
|
||||
<tr>
|
||||
<td style="padding:0.2rem 0.5rem 0.2rem 0;word-break:break-all;">
|
||||
{{ c.hostname }}
|
||||
{% if c.type == "wireless" and c.signal %}
|
||||
<span style="color:var(--text-dim);font-size:0.75rem;"> {{ c.signal }}dBm</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="text-align:right;padding:0.2rem 0;white-space:nowrap;font-size:0.78rem;">
|
||||
{% set rx = c.rx_rate %}{% set tx = c.tx_rate %}
|
||||
<span style="color:var(--text-muted);">↓</span>
|
||||
{% if rx >= 1048576 %}{{ "%.1f"|format(rx/1048576) }}M
|
||||
{% elif rx >= 1024 %}{{ "%.0f"|format(rx/1024) }}K
|
||||
{% else %}{{ "%.0f"|format(rx) }}B{% endif %}
|
||||
<span style="color:var(--text-muted);margin-left:0.3rem;">↑</span>
|
||||
{% if tx >= 1048576 %}{{ "%.1f"|format(tx/1048576) }}M
|
||||
{% elif tx >= 1024 %}{{ "%.0f"|format(tx/1024) }}K
|
||||
{% else %}{{ "%.0f"|format(tx) }}B{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# WLAN SSIDs card #}
|
||||
{% if wlan_latest %}
|
||||
<div class="card" style="break-inside:avoid;margin-bottom:1rem;">
|
||||
<div class="section-title" style="margin-bottom:0.75rem;">Wireless Networks</div>
|
||||
<table class="table" style="font-size:0.83rem;width:100%;">
|
||||
<tr style="color:var(--text-muted);font-size:0.78rem;">
|
||||
<th style="text-align:left;font-weight:normal;padding:0 0.5rem 0.3rem 0;">SSID</th>
|
||||
<th style="text-align:right;font-weight:normal;padding:0 0.3rem 0.3rem;">Clients</th>
|
||||
<th style="text-align:right;font-weight:normal;padding:0 0 0.3rem;">Score</th>
|
||||
</tr>
|
||||
{% for w in wlan_latest %}
|
||||
<tr>
|
||||
<td style="padding:0.2rem 0.5rem 0.2rem 0;">{{ w.ssid }}</td>
|
||||
<td style="text-align:right;padding:0.2rem 0.3rem;">{{ w.num_sta }}</td>
|
||||
<td style="text-align:right;padding:0.2rem 0;
|
||||
{% if w.satisfaction is not none %}
|
||||
{% if w.satisfaction >= 80 %}color:var(--green){% elif w.satisfaction >= 60 %}color:var(--yellow){% else %}color:var(--red){% endif %}
|
||||
{% endif %};">
|
||||
{% if w.satisfaction is not none %}{{ w.satisfaction }}%{% else %}—{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Speedtest card #}
|
||||
{% if speedtest %}
|
||||
<div class="card" style="break-inside:avoid;margin-bottom:1rem;">
|
||||
<div class="section-title" style="margin-bottom:0.75rem;">Last Speed Test</div>
|
||||
<div style="display:flex;gap:1.5rem;font-size:0.88rem;font-variant-numeric:tabular-nums;">
|
||||
{% if speedtest.download_mbps is not none %}
|
||||
<span>
|
||||
<span style="color:var(--text-muted);font-size:0.8rem;">↓ DL</span>
|
||||
<span style="font-weight:600;margin-left:0.3rem;">{{ "%.1f"|format(speedtest.download_mbps) }}</span>
|
||||
<span style="color:var(--text-muted);font-size:0.78rem;">Mbps</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if speedtest.upload_mbps is not none %}
|
||||
<span>
|
||||
<span style="color:var(--text-muted);font-size:0.8rem;">↑ UL</span>
|
||||
<span style="font-weight:600;margin-left:0.3rem;">{{ "%.1f"|format(speedtest.upload_mbps) }}</span>
|
||||
<span style="color:var(--text-muted);font-size:0.78rem;">Mbps</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if speedtest.latency_ms is not none %}
|
||||
<span>
|
||||
<span style="color:var(--text-muted);font-size:0.8rem;">Latency</span>
|
||||
<span style="margin-left:0.3rem;">{{ "%.0f"|format(speedtest.latency_ms) }}ms</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if speedtest.server_name %}
|
||||
<div style="margin-top:0.4rem;color:var(--text-dim);font-size:0.78rem;">via {{ speedtest.server_name }}</div>
|
||||
{% endif %}
|
||||
<div style="margin-top:0.25rem;color:var(--text-dim);font-size:0.75rem;">{{ speedtest.run_at.strftime("%Y-%m-%d %H:%M") }} UTC</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
{% if not latest_wan and not devices %}
|
||||
<div class="card">
|
||||
<p style="color:var(--text-muted);">No UniFi data yet. Check your <code>host</code>, <code>username</code>, and <code>password</code> in the plugin config.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,56 @@
|
||||
{# plugins/unifi/templates/unifi/security.html #}
|
||||
<div style="column-width:340px;column-count:2;column-gap:1rem;">
|
||||
|
||||
{# Active alarms #}
|
||||
<div class="card" style="break-inside:avoid;margin-bottom:1rem;">
|
||||
<div class="section-title" style="margin-bottom:0.75rem;">
|
||||
Active Alarms
|
||||
{% if active_alarms %}
|
||||
<span style="float:right;background:var(--red);color:#fff;font-size:0.72rem;padding:0.1rem 0.45rem;border-radius:10px;font-weight:600;">{{ active_alarms|length }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if not active_alarms %}
|
||||
<p style="color:var(--green);font-size:0.88rem;">✓ No active alarms</p>
|
||||
{% else %}
|
||||
<table class="table" style="width:100%;font-size:0.82rem;">
|
||||
{% for al in active_alarms %}
|
||||
<tr>
|
||||
<td style="padding:0.3rem 0.5rem 0.3rem 0;">
|
||||
<div style="color:var(--red);font-size:0.78rem;margin-bottom:0.1rem;">
|
||||
⚠ {{ al.alarm_key }}
|
||||
</div>
|
||||
<div>{{ al.message }}</div>
|
||||
<div style="color:var(--text-dim);font-size:0.75rem;margin-top:0.1rem;">
|
||||
{{ al.alarm_time.strftime("%Y-%m-%d %H:%M") }} UTC
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Recent archived alarms #}
|
||||
{% if archived_alarms %}
|
||||
<div class="card" style="break-inside:avoid;margin-bottom:1rem;">
|
||||
<div class="section-title" style="margin-bottom:0.75rem;">
|
||||
Recent Archived Alarms
|
||||
<span style="float:right;font-weight:normal;font-size:0.78rem;color:var(--text-dim);">last {{ archived_alarms|length }}</span>
|
||||
</div>
|
||||
<table class="table" style="width:100%;font-size:0.82rem;">
|
||||
{% for al in archived_alarms %}
|
||||
<tr>
|
||||
<td style="padding:0.25rem 0.5rem 0.25rem 0;">
|
||||
<div style="color:var(--text-muted);font-size:0.78rem;">{{ al.alarm_key }}</div>
|
||||
<div style="color:var(--text-dim);font-size:0.8rem;">{{ al.message }}</div>
|
||||
</td>
|
||||
<td style="padding:0.25rem 0;text-align:right;color:var(--text-dim);font-size:0.75rem;white-space:nowrap;">
|
||||
{{ al.alarm_time.strftime("%m-%d %H:%M") }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,95 @@
|
||||
{# plugins/unifi/templates/unifi/widget.html #}
|
||||
|
||||
{# Active alarms #}
|
||||
{% for al in active_alarms %}
|
||||
<div style="display:flex;justify-content:space-between;font-size:0.8rem;color:var(--red);padding:0.15rem 0;">
|
||||
<span>⚠ {{ al.alarm_key }}</span>
|
||||
<span style="color:var(--text-dim);font-size:0.75rem;">alarm</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{# Offline device alerts #}
|
||||
{% for d in offline_devices %}
|
||||
<div style="display:flex;justify-content:space-between;font-size:0.8rem;color:var(--red);padding:0.15rem 0;">
|
||||
<span>⚠ {{ d.name or d.mac }}</span>
|
||||
<span>offline</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if active_alarms or offline_devices %}<div style="height:0.4rem;"></div>{% endif %}
|
||||
|
||||
{% if latest_wan %}
|
||||
<div class="ping-row">
|
||||
<div class="ping-meta">
|
||||
<span class="ping-name" style="font-size:0.8rem;">WAN</span>
|
||||
</div>
|
||||
<div style="flex:1;display:flex;gap:1.5rem;font-size:0.82rem;font-variant-numeric:tabular-nums;">
|
||||
<span style="color:{% if latest_wan.is_up %}var(--green){% else %}var(--red){% endif %};">
|
||||
{% if latest_wan.is_up %}Up{% else %}Down{% endif %}
|
||||
</span>
|
||||
{% if latest_wan.latency_ms is not none %}
|
||||
<span>
|
||||
<span style="color:var(--text-muted);">lat</span>
|
||||
<span style="margin-left:0.2rem;{% if latest_wan.latency_ms > 100 %}color:var(--red){% elif latest_wan.latency_ms > 50 %}color:var(--yellow){% endif %};">
|
||||
{{ "%.0f"|format(latest_wan.latency_ms) }}ms
|
||||
</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if latest_wan.rx_bytes_rate is not none %}
|
||||
<span>
|
||||
<span style="color:var(--text-muted);">↓</span>
|
||||
{% set bps = latest_wan.rx_bytes_rate %}
|
||||
{% if bps >= 1048576 %}{{ "%.1f"|format(bps / 1048576) }}MB/s
|
||||
{% elif bps >= 1024 %}{{ "%.0f"|format(bps / 1024) }}KB/s
|
||||
{% else %}{{ "%.0f"|format(bps) }}B/s{% endif %}
|
||||
</span>
|
||||
<span>
|
||||
<span style="color:var(--text-muted);">↑</span>
|
||||
{% set bps = latest_wan.tx_bytes_rate %}
|
||||
{% if bps >= 1048576 %}{{ "%.1f"|format(bps / 1048576) }}MB/s
|
||||
{% elif bps >= 1024 %}{{ "%.0f"|format(bps / 1024) }}KB/s
|
||||
{% else %}{{ "%.0f"|format(bps) }}B/s{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if latest_clients %}
|
||||
<div class="ping-row">
|
||||
<div class="ping-meta">
|
||||
<span class="ping-name" style="font-size:0.8rem;">Clients</span>
|
||||
</div>
|
||||
<div style="flex:1;display:flex;gap:1rem;font-size:0.82rem;">
|
||||
<span style="font-weight:600;">{{ latest_clients.total_clients }}</span>
|
||||
<span style="color:var(--text-muted);">{{ latest_clients.wireless_clients }}w / {{ latest_clients.wired_clients }}e</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if top_clients %}
|
||||
<div style="margin-top:0.4rem;border-top:1px solid var(--border);padding-top:0.4rem;">
|
||||
{% for c in top_clients %}
|
||||
<div class="ping-row" style="padding:0.1rem 0;">
|
||||
<div class="ping-meta" style="min-width:0;flex:1;">
|
||||
<span style="font-size:0.78rem;color:var(--text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">{{ c.hostname }}</span>
|
||||
</div>
|
||||
<div style="font-size:0.78rem;font-variant-numeric:tabular-nums;white-space:nowrap;color:var(--text-muted);">
|
||||
{% set total_bps = c.tx_rate + c.rx_rate %}
|
||||
<span style="color:var(--text-muted);">↓</span>
|
||||
{% set rx = c.rx_rate %}
|
||||
{% if rx >= 1048576 %}{{ "%.1f"|format(rx / 1048576) }}M
|
||||
{% elif rx >= 1024 %}{{ "%.0f"|format(rx / 1024) }}K
|
||||
{% else %}{{ "%.0f"|format(rx) }}B{% endif %}
|
||||
<span style="color:var(--text-muted);margin-left:0.4rem;">↑</span>
|
||||
{% set tx = c.tx_rate %}
|
||||
{% if tx >= 1048576 %}{{ "%.1f"|format(tx / 1048576) }}M
|
||||
{% elif tx >= 1024 %}{{ "%.0f"|format(tx / 1024) }}K
|
||||
{% else %}{{ "%.0f"|format(tx) }}B{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if not latest_wan and not latest_clients and not active_alarms and not offline_devices %}
|
||||
<p class="empty" style="padding:0.5rem 0;font-size:0.85rem;">No UniFi data yet.</p>
|
||||
{% endif %}
|
||||
Reference in New Issue
Block a user