Files
agessaman a594b72a85 fix(neighbors): update zero-hop neighbor discovery and data handling
This commit enhances the handling of zero-hop neighbors in the dashboard and database. It ensures that the **One-hop neighbours** section accurately reflects radios heard directly (MeshCore hop count 0) rather than originators of relayed adverts. The `observed_paths` table now includes nullable `snr` and `rssi` columns for zero-hop advert rows, allowing for better signal reporting. Additionally, a one-time backfill process copies recent zero-hop ADVERTs from the `packet_stream` to `observed_paths`. Documentation and tests have been updated to reflect these changes.
2026-08-12 18:01:28 -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">heard directly by this radio</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 %}