Files
meshcore-bot/modules/web_viewer/templates/index.html
T
agessaman 7ea34a2672 feat(web-viewer): hide flood hop buckets under 0.1% of the series
The flood tail decays over roughly twenty hops in bars under a pixel
tall. Buckets holding less than 0.1% of the series are no longer drawn,
which on the live mesh takes the axis from 64 buckets to 44.

What is left out is reported, not dropped: a line under the chart reads
"1,467 further flood packets (0.9%) sit in 20 hop buckets below 0.1%
each, and are not drawn." An unannounced cut would be the same lie as a
silently truncated category list.

Two details that matter for honesty:

Percentages still divide by the whole series, never by the drawn subset,
so removing the tail cannot inflate the bars that remain. The smallest
surviving bar on live data is 0.118%, which is what it was before.

A withheld bucket inside the axis is null rather than zero. Zero would
claim no packets travelled that far, which is a different and false
statement; null draws nothing and says nothing. Padding gaps stay zero,
because there it is true.

The threshold applies to the flood series only — the node series is
small enough to draw in full — and the underlying computation still
covers the whole 64-hop protocol range.
2026-07-29 23:16:51 -07:00

416 lines
19 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}Dashboard - MeshCore Bot Data Viewer{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/dashboard.css') }}">
{% endblock %}
{% block content %}
<!-- Health strip — replaces the old four-column health card -->
<div class="health-strip">
<span class="health-strip__item">
<span class="status-indicator status-warning" id="health-status-dot"></span>
<strong id="health-status-text">Connecting…</strong>
</span>
<span class="health-strip__item" id="health-db">DB —</span>
<span class="health-strip__item">
<span class="health-strip__label">Uptime</span>
<span id="health-uptime"></span>
</span>
<span class="health-strip__item">
<a href="#" id="health-clients-link" class="text-decoration-none"
data-bs-toggle="modal" data-bs-target="#connectedClientsModal">
<span id="health-clients">0</span> clients
</a>
</span>
<span class="health-strip__item" id="health-radio">radio —</span>
<span class="health-strip__spacer"></span>
<span class="health-strip__item">
<span id="health-snapshot">snapshot —</span>
<button type="button" class="btn-refresh" id="snapshot-refresh"
title="Recompute the dashboard snapshot now" aria-label="Refresh snapshot">
<i class="fas fa-sync-alt" aria-hidden="true"></i>
</button>
</span>
</div>
<!-- ══ Mesh ══════════════════════════════════════════════════════════════ -->
<h2 class="dashboard-section-title"><i class="fas fa-project-diagram"></i> Mesh</h2>
<!-- Row 1 — stat tiles (five, so col-xl auto-splits them evenly) -->
<div class="row g-3 mb-1">
<div class="col-6 col-md-4 col-xl">
<div class="stat-tile">
<div class="stat-tile__label">Nodes heard 24h</div>
<div class="stat-tile__row">
<span class="stat-tile__value" id="nodes-24h-value"></span>
<span class="delta-chip" id="nodes-24h-delta"></span>
</div>
<div class="stat-tile__spark"><canvas id="nodes-24h-spark"
aria-label="Nodes heard per day, last 30 days"></canvas></div>
</div>
</div>
<div class="col-6 col-md-4 col-xl">
<div class="stat-tile">
<div class="stat-tile__label">Adverts 24h</div>
<div class="stat-tile__row">
<span class="stat-tile__value" id="adverts-24h-value"></span>
<span class="delta-chip" id="adverts-24h-delta"></span>
</div>
<div class="stat-tile__spark"><canvas id="adverts-24h-spark"
aria-label="Adverts per day, last 30 days"></canvas></div>
</div>
</div>
<div class="col-6 col-md-4 col-xl">
<div class="stat-tile">
<div class="stat-tile__label">New nodes 24h</div>
<div class="stat-tile__row">
<span class="stat-tile__value" id="new-nodes-value"></span>
<span class="delta-chip" id="new-nodes-delta"></span>
</div>
<div class="stat-tile__sub" id="new-nodes-7d"></div>
<div class="stat-tile__spark"><canvas id="new-nodes-spark"
aria-label="New nodes per day, last 30 days"></canvas></div>
</div>
</div>
<div class="col-6 col-md-4 col-xl">
<div class="stat-tile">
<div class="stat-tile__label">Gone quiet 7d</div>
<div class="stat-tile__row">
<span class="stat-tile__value text-warning" id="gone-quiet"></span>
</div>
<div class="stat-tile__sub">Heard last week, silent this week</div>
</div>
</div>
<div class="col-6 col-md-4 col-xl">
<div class="stat-tile">
<div class="stat-tile__label">Coverage</div>
<div class="stat-tile__row">
<span class="stat-tile__value" id="coverage-countries"></span>
<span class="stat-tile__sub">countries</span>
</div>
<div class="stat-tile__sub">
<span id="coverage-states"></span> states ·
<span id="coverage-cities"></span> cities
</div>
<div class="stat-tile__sub"><span id="coverage-known"></span> contacts known</div>
</div>
</div>
</div>
<p class="dashboard-note mb-3">
Tile values are rolling 24-hour counts; the
<span class="delta-chip"></span> chip compares the last two complete calendar days.
<button type="button" class="dashboard-info-btn" id="delta-info"
aria-label="How the change figure is calculated"
data-bs-toggle="tooltip" data-bs-placement="top">
<i class="fas fa-info-circle" aria-hidden="true"></i>
</button>
</p>
<!-- Row 2 — routing health -->
<div class="row g-3 mb-3">
<div class="col-12 col-lg-5">
<div class="card h-100">
<div class="card-header"><i class="fas fa-route"></i> Routing mix</div>
<div class="card-body">
<p class="dashboard-note mb-2">
Incoming packets, <span id="route-mix-window">no data</span>
</p>
<div class="stacked-bar" id="route-mix-bar"></div>
<div class="stacked-bar__legend" id="route-mix-legend"></div>
<hr class="my-3">
<p class="dashboard-note mb-2">Payload types over the same packets</p>
<div id="payload-mix"><div class="dashboard-empty">Loading…</div></div>
</div>
</div>
</div>
<div class="col-12 col-lg-4">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="fas fa-ruler-horizontal"></i> Hops away</span>
<button type="button" class="dashboard-info-btn" id="hops-info"
aria-label="How hop distance is derived"
data-bs-toggle="tooltip" data-bs-placement="top">
<i class="fas fa-info-circle" aria-hidden="true"></i>
</button>
</div>
<div class="card-body card-body--fill">
<p class="dashboard-note mb-2">
Nodes by their closest advert path, and how far arriving
flood packets had travelled
</p>
<div class="chart-box chart-box--fill">
<canvas id="hopsHistogramChart"
aria-label="Nodes and flood packets by hop distance"></canvas>
</div>
<p class="dashboard-note mt-2 mb-0" id="hops-hidden-note"></p>
</div>
</div>
</div>
<div class="col-12 col-lg-3">
<div class="card h-100">
<div class="card-header"><i class="fas fa-microchip"></i> Roles</div>
<div class="card-body card-body--fill">
<div id="role-mix" class="mix-fill"><div class="dashboard-empty">Loading…</div></div>
</div>
</div>
</div>
</div>
<!-- Row 3 — path encoding -->
<div class="row g-3 mb-3">
<div class="col-12 col-lg-7">
<div class="card h-100">
<div class="card-header"><i class="fas fa-chart-pie"></i> Path encoding</div>
<div class="card-body">
<div class="row g-3">
<div class="col-12 col-md-6 text-center">
<p class="dashboard-note mb-2">
Contacts (last 7 days)
<button type="button" class="dashboard-info-btn" id="contacts-encoding-info"
aria-label="How multibyte capable contacts are counted"
data-bs-toggle="tooltip" data-bs-placement="top">
<i class="fas fa-info-circle" aria-hidden="true"></i>
</button>
</p>
<div class="doughnut-box">
<canvas id="contactsEncodingChart"
aria-label="Multibyte path share among contacts, last 7 days"></canvas>
</div>
<p class="dashboard-note mt-2 mb-0" id="contacts-encoding-summary"></p>
</div>
<div class="col-12 col-md-6 text-center">
<p class="dashboard-note mb-2">
Incoming packets (<span id="packets-window-label">no data</span>)
</p>
<div class="doughnut-box">
<canvas id="packetsEncodingChart"
aria-label="Multibyte path share among incoming packets"></canvas>
</div>
<p class="dashboard-note mt-2 mb-0" id="packets-encoding-summary"></p>
</div>
</div>
</div>
</div>
</div>
<div class="col-12 col-lg-5">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="fas fa-chart-line"></i> Multibyte share of adverts</span>
<button type="button" class="dashboard-info-btn" id="multibyte-trend-info"
aria-label="How the multibyte trend is computed"
data-bs-toggle="tooltip" data-bs-placement="top">
<i class="fas fa-info-circle" aria-hidden="true"></i>
</button>
</div>
<div class="card-body">
<div class="chart-box chart-box--tall">
<canvas id="multibyteTrendChart"
aria-label="Multibyte share of daily adverts, last 30 days"></canvas>
<div class="dashboard-empty" id="multibyte-trend-empty" style="display:none">
No classified advert days yet — the trend starts accumulating from today.
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Row 4 — busiest repeaters, role &amp; device mix -->
<div class="row g-3 mb-3">
<div class="col-12 col-lg-6">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="fas fa-tower-broadcast"></i> Busiest repeaters</span>
<select id="top-repeaters-window" class="form-select form-select-sm"
style="width:auto;font-size:0.75rem" aria-label="Repeater time window">
<option value="7d">Last 7 days</option>
</select>
</div>
<div class="card-body">
<div id="top-repeaters-list"><div class="dashboard-empty">Loading…</div></div>
</div>
</div>
</div>
<div class="col-12 col-lg-6">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<span>
<i class="fas fa-signal"></i> One-hop neighbours
<button type="button" class="dashboard-info-btn" id="neighbors-info"
aria-label="What counts as a one-hop neighbour"
data-bs-toggle="tooltip" data-bs-placement="top">
<i class="fas fa-info-circle" aria-hidden="true"></i>
</button>
</span>
<select id="neighbors-window" class="form-select form-select-sm"
style="width:auto;font-size:0.75rem" aria-label="Neighbour time window">
<option value="24h">Last 24 hours</option>
</select>
</div>
<div class="card-body">
<div class="d-flex align-items-baseline gap-2 flex-wrap mb-1">
<span class="stat-tile__value" id="neighbors-count"></span>
<span class="dashboard-note">nodes reached this radio in a single hop</span>
</div>
<p class="dashboard-note mb-3" id="neighbors-shown"></p>
<div id="neighbors-list"><div class="dashboard-empty">Loading…</div></div>
</div>
</div>
</div>
</div>
<!-- ══ Bot ═══════════════════════════════════════════════════════════════ -->
<h2 class="dashboard-section-title"><i class="fas fa-robot"></i> Bot</h2>
<p class="dashboard-note text-warning mb-2" id="bot-source-note" style="display:none"></p>
<!-- Row 5 — bot tiles -->
<div class="row g-3 mb-3">
<div class="col-6 col-lg-3">
<div class="stat-tile">
<div class="stat-tile__label">Messages 24h</div>
<div class="stat-tile__row">
<span class="stat-tile__value" id="messages-24h-value"></span>
<span class="delta-chip" id="messages-24h-delta"></span>
</div>
<div class="stat-tile__spark"><canvas id="messages-24h-spark"
aria-label="Messages per day, last 30 days"></canvas></div>
</div>
</div>
<div class="col-6 col-lg-3">
<div class="stat-tile">
<div class="stat-tile__label">Commands 24h</div>
<div class="stat-tile__row">
<span class="stat-tile__value" id="commands-24h-value"></span>
<span class="delta-chip" id="commands-24h-delta"></span>
</div>
<div class="stat-tile__spark"><canvas id="commands-24h-spark"
aria-label="Commands per day, last 30 days"></canvas></div>
</div>
</div>
<div class="col-6 col-lg-3">
<div class="stat-tile">
<div class="stat-tile__label">Reply rate 24h</div>
<div class="stat-tile__row">
<span class="stat-tile__value" id="reply-rate-24h"></span>
</div>
<div class="stat-tile__sub">Commands that received a response</div>
</div>
</div>
<div class="col-6 col-lg-3">
<div class="stat-tile">
<div class="stat-tile__label">Unique users 24h</div>
<div class="stat-tile__row">
<span class="stat-tile__value" id="unique-users-24h"></span>
</div>
<div class="stat-tile__sub" id="unique-channels-24h"></div>
</div>
</div>
</div>
<!-- Row 6 — leaderboards -->
<div class="row g-3 mb-3">
<div class="col-12 col-md-6 col-xl-3">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="fas fa-trophy"></i> Top commands</span>
<select id="top-commands-window" class="form-select form-select-sm"
style="width:auto;font-size:0.75rem" aria-label="Command time window">
<option value="all">All retained</option>
</select>
</div>
<div class="card-body">
<div id="top-commands-list"><div class="dashboard-empty">Loading…</div></div>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-xl-3">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="fas fa-users"></i> Top users</span>
<select id="top-users-window" class="form-select form-select-sm"
style="width:auto;font-size:0.75rem" aria-label="User time window">
<option value="all">All retained</option>
</select>
</div>
<div class="card-body">
<div id="top-users-list"><div class="dashboard-empty">Loading…</div></div>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-xl-3">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="fas fa-hashtag"></i> Top channels</span>
<select id="top-channels-window" class="form-select form-select-sm"
style="width:auto;font-size:0.75rem" aria-label="Channel time window">
<option value="all">All retained</option>
</select>
</div>
<div class="card-body">
<div id="top-channels-list"><div class="dashboard-empty">Loading…</div></div>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-xl-3">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="fas fa-route"></i> Longest paths</span>
<select id="top-paths-window" class="form-select form-select-sm"
style="width:auto;font-size:0.75rem" aria-label="Path time window">
<option value="all">All retained</option>
</select>
</div>
<div class="card-body">
<div id="top-paths-list"><div class="dashboard-empty">Loading…</div></div>
</div>
</div>
</div>
</div>
<!-- Connected Clients Modal -->
<div class="modal fade" id="connectedClientsModal" tabindex="-1"
aria-labelledby="connectedClientsModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="connectedClientsModalLabel">
<i class="fas fa-users me-2"></i>Connected Clients
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="connected-clients-loading" class="text-center py-3">
<div class="spinner-border spinner-border-sm" role="status"></div>
<span class="ms-2">Loading…</span>
</div>
<div id="connected-clients-empty" class="text-center text-muted py-3" style="display:none">
No clients currently connected.
</div>
<table class="table table-sm mb-0" id="connected-clients-table" style="display:none">
<thead>
<tr>
<th>Client ID</th>
<th>Connected At</th>
<th>Last Activity</th>
</tr>
</thead>
<tbody id="connected-clients-tbody"></tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
{# Data, not code: a JSON block needs no CSP nonce and cannot execute. #}
<script type="application/json" id="dashboard-boot">{{ dashboard_boot|tojson }}</script>
<script src="{{ url_for('static', filename='js/dashboard.js') }}"></script>
{% endblock %}