mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-08-27 21:10:13 +00:00
- Added a new feature to track and display the multibyte share of packets by payload type in the dashboard. - Introduced a new database migration to store per-payload-type multibyte encoding data in the daily rollup. - Updated the dashboard to visualize the multibyte share as a stacked bar chart, reflecting the share of each day's packets that took a multibyte path. - Enhanced the API to provide raw counts for each payload type, ensuring accurate representation in the dashboard. - Adjusted the frontend to maintain consistent color coding for payload types and improve the overall user experience. - Updated tests to validate the new multibyte share functionality and ensure data integrity.
419 lines
19 KiB
HTML
419 lines
19 KiB
HTML
{% 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 Packets</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">
|
||
<p class="dashboard-note mb-2">
|
||
Share of each day's packets that took a multibyte path, by payload type
|
||
</p>
|
||
<div class="chart-box chart-box--tall">
|
||
<canvas id="multibyteTrendChart"
|
||
aria-label="Daily multibyte packets by payload type, last 30 days"></canvas>
|
||
<div class="dashboard-empty" id="multibyte-trend-empty" style="display:none">
|
||
No classified packet days yet — the chart starts accumulating from today.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Row 4 — busiest repeaters, role & 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 %}
|