Files
agessaman 75fda424c2 feat(web-viewer): implement per-payload-type multibyte share tracking
- 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.
2026-07-31 21:23:51 -07:00

419 lines
19 KiB
HTML
Raw Permalink 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 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 &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 %}