mirror of
https://github.com/Roslund/sthlm-mesh.git
synced 2026-08-06 17:02:56 +02:00
deploy: 14689c6bfa
This commit is contained in:
@@ -233,6 +233,28 @@ Projektet drivs av entusiaster som vill utforska möjligheterna med decentralise
|
||||
<section class="row td-box td-box--white td-box--height-auto">
|
||||
<div class="col">
|
||||
<div class="container">
|
||||
<style>
|
||||
@media (max-width: 576px) {
|
||||
#channelToolbar { top: 16px !important; }
|
||||
}
|
||||
</style>
|
||||
<div id="channelToolbar" class="d-flex justify-content-end sticky-top" style="top: 84px;">
|
||||
<div class="btn-toolbar bg-white shadow-sm rounded-3 py-2 px-2" role="toolbar" aria-label="Channel toolbar">
|
||||
<div class="btn-group btn-group-sm me-2" role="group" aria-label="Channel filter">
|
||||
<input type="radio" class="btn-check" name="btnchannel-nodes" id="filterChannelAll" autocomplete="off" checked>
|
||||
<label class="btn btn-outline-primary" for="filterChannelAll">Alla</label>
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm me-2" role="group" aria-label="Basic radio toggle button group">
|
||||
<input type="radio" class="btn-check" name="btnchannel-nodes" id="filterChannelMediumFast" data-channel="MediumFast" autocomplete="off">
|
||||
<label class="btn btn-outline-primary" for="filterChannelMediumFast">MediumFast</label>
|
||||
<input type="radio" class="btn-check" name="btnchannel-nodes" id="filterChannelLongFast" data-channel="LongFast" autocomplete="off">
|
||||
<label class="btn btn-outline-primary" for="filterChannelLongFast">LongFast</label>
|
||||
</div>
|
||||
<div data-bs-toggle="tooltip" data-bs-placement="left" title="Filtrera på kanal eller visa alla.">
|
||||
<span class="badge rounded-pill bg-dark">?</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2 id="antal-enheter">Antal Enheter<a class="td-heading-self-link" href="#antal-enheter" aria-label="Heading self-link"></a></h2>
|
||||
<div class="container my-3 mx-0" style="max-width: 1000px;">
|
||||
<div class="row text-center px-0">
|
||||
@@ -242,18 +264,4 @@ Projektet drivs av entusiaster som vill utforska möjligheterna med decentralise
|
||||
</div>
|
||||
<div class="col">
|
||||
<h2 class="display-4 text-warning fw-bold" id="count-7">–</h2>
|
||||
<p class="text-muted">Senaste 7 dagarna</p>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h2 class="display-4 text-success fw-bold" id="count-30">–</h2>
|
||||
<p class="text-muted">Senaste 30 dagarna</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2 id="text-meddelanden">Text Meddelanden<a class="td-heading-self-link" href="#text-meddelanden" aria-label="Heading self-link"></a></h2>
|
||||
<p>Antalet meddelanden per timme senaste 7 dygnen. Grafen visar meddelanden som skickats på LongFast kanalen, men även okrypterade meddelanden mellan noder. De meddelanden som skickas går att se <a href="https://sthlm-mesh.se/messages/">här</a>.</p>
|
||||
<div class="stats-chart-container" style="min-height:300px;">
|
||||
<canvas id="messagesChart"></canvas>
|
||||
</div>
|
||||
<h2 id="kanalutnyttjande">Kanalutnyttjande<a class="td-heading-self-link" href="#kanalutnyttjande" aria-label="Heading self-link"></a></h2>
|
||||
<p>Mätarna visar genomsnittligt kanalutnyttjande för de 10 noder som har högst kanalutnyttjande. Kanalutnyttjande (Channel Utilization) visar hur mycket av radiokanalen som är upptagen. Under 15–20% är allt normalt, över 25% får Telemetri lägre prioritet, vilket kan ge längre tid mellan uppdateringar. Runt 40% stryps positionsuppdateringar. Här i EU gäller dessutom max 10% sändtid per timme som kan pausa sändningar helt.</p></description></item></channel></rss>
|
||||
<p class="text-muted">Senaste 7 dagarna</p></description></item></channel></rss>
|
||||
@@ -1,3 +1,6 @@
|
||||
let deviceRolesChartInstance = null;
|
||||
let selectedChannelIdRoles = null; // null = All
|
||||
|
||||
async function deviceRolesChart() {
|
||||
const canvas = document.getElementById('deviceRoles');
|
||||
const ctx = canvas.getContext('2d');
|
||||
@@ -14,7 +17,12 @@ async function deviceRolesChart() {
|
||||
|
||||
// Count role occurrences
|
||||
const roleCounts = {};
|
||||
nodes.forEach(node => {
|
||||
const filteredNodes = Array.isArray(nodes) ? nodes.filter(n => {
|
||||
if (!selectedChannelIdRoles) return true; // All
|
||||
return n.channel_id === selectedChannelIdRoles;
|
||||
}) : [];
|
||||
|
||||
filteredNodes.forEach(node => {
|
||||
const role = node.role_name || "UNKNOWN";
|
||||
roleCounts[role] = (roleCounts[role] || 0) + 1;
|
||||
});
|
||||
@@ -36,7 +44,12 @@ async function deviceRolesChart() {
|
||||
return 'rgb(41, 156, 70)';
|
||||
});
|
||||
|
||||
new Chart(ctx, {
|
||||
if (deviceRolesChartInstance) {
|
||||
deviceRolesChartInstance.destroy();
|
||||
deviceRolesChartInstance = null;
|
||||
}
|
||||
|
||||
deviceRolesChartInstance = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
@@ -71,3 +84,14 @@ async function deviceRolesChart() {
|
||||
}
|
||||
|
||||
deviceRolesChart();
|
||||
|
||||
// Listen via shared StatusFilter
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (typeof StatusFilter !== 'undefined' && StatusFilter.subscribe) {
|
||||
selectedChannelIdRoles = StatusFilter.getChannelId ? StatusFilter.getChannelId() : null;
|
||||
StatusFilter.subscribe(() => {
|
||||
selectedChannelIdRoles = StatusFilter.getChannelId ? StatusFilter.getChannelId() : null;
|
||||
deviceRolesChart();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,8 +10,11 @@ async function firmwareVersionGraph() {
|
||||
|
||||
try {
|
||||
await fetchNodes();
|
||||
// Filter out nodes without firmware_version
|
||||
const nodesWithVersion = nodes.filter(node => node.firmware_version != null);
|
||||
const channelId = (typeof StatusFilter !== 'undefined' && StatusFilter.getChannelId) ? StatusFilter.getChannelId() : null;
|
||||
// Filter out nodes without firmware_version and channel match
|
||||
const nodesWithVersion = nodes
|
||||
.filter(node => node.firmware_version != null)
|
||||
.filter(node => !channelId || node.channel_id === channelId);
|
||||
|
||||
const countsByVersion = {};
|
||||
const validVersions = ['<2.5.0', '>2.5.0', '>2.6.8'];
|
||||
@@ -39,7 +42,12 @@ async function firmwareVersionGraph() {
|
||||
const chartContainer = document.getElementById('firmwareVersionContainer');
|
||||
chartContainer.style.height = `${labels.length * 35 + 50}px`;
|
||||
|
||||
new Chart(ctx, {
|
||||
if (window.firmwareVersionChartInstance) {
|
||||
window.firmwareVersionChartInstance.destroy();
|
||||
window.firmwareVersionChartInstance = null;
|
||||
}
|
||||
|
||||
window.firmwareVersionChartInstance = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
@@ -67,3 +75,10 @@ async function firmwareVersionGraph() {
|
||||
}
|
||||
|
||||
firmwareVersionGraph();
|
||||
|
||||
// Re-render on channel change
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (typeof StatusFilter !== 'undefined' && StatusFilter.subscribe) {
|
||||
StatusFilter.subscribe(() => firmwareVersionGraph());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11,8 +11,11 @@ async function isOkToMqttGraph() {
|
||||
|
||||
try {
|
||||
await fetchNodes();
|
||||
// Filter nodes to only include those updated within the last 30 days
|
||||
const recentNodes = nodes.filter(node => new Date(node.updated_at) >= new Date(Date.now() - 30 * 24 * 60 * 60 * 1000));
|
||||
const channelId = (typeof StatusFilter !== 'undefined' && StatusFilter.getChannelId) ? StatusFilter.getChannelId() : null;
|
||||
// Filter nodes to only include those updated within the last 30 days and channel match
|
||||
const recentNodes = nodes
|
||||
.filter(node => new Date(node.updated_at) >= new Date(Date.now() - 30 * 24 * 60 * 60 * 1000))
|
||||
.filter(node => !channelId || node.channel_id === channelId);
|
||||
|
||||
let countTrue = 0;
|
||||
let countFalse = 0;
|
||||
@@ -30,7 +33,12 @@ async function isOkToMqttGraph() {
|
||||
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
new Chart(ctx, {
|
||||
if (window.isOkToMqttChartInstance) {
|
||||
window.isOkToMqttChartInstance.destroy();
|
||||
window.isOkToMqttChartInstance = null;
|
||||
}
|
||||
|
||||
window.isOkToMqttChartInstance = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['True', 'False'],
|
||||
@@ -77,4 +85,11 @@ async function isOkToMqttGraph() {
|
||||
|
||||
isOkToMqttGraph();
|
||||
|
||||
// Re-render on channel change
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (typeof StatusFilter !== 'undefined' && StatusFilter.subscribe) {
|
||||
StatusFilter.subscribe(() => isOkToMqttGraph());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -11,8 +11,11 @@ async function isUnmessagableGraph() {
|
||||
|
||||
try {
|
||||
await fetchNodes();
|
||||
// Filter nodes to only include those updated within the last 30 days
|
||||
const recentNodes = nodes.filter(node => new Date(node.updated_at) >= new Date(Date.now() - 30 * 24 * 60 * 60 * 1000));
|
||||
const channelId = (typeof StatusFilter !== 'undefined' && StatusFilter.getChannelId) ? StatusFilter.getChannelId() : null;
|
||||
// Filter nodes to only include those updated within the last 30 days and channel match
|
||||
const recentNodes = nodes
|
||||
.filter(node => new Date(node.updated_at) >= new Date(Date.now() - 30 * 24 * 60 * 60 * 1000))
|
||||
.filter(node => !channelId || node.channel_id === channelId);
|
||||
|
||||
let countTrue = 0;
|
||||
let countFalse = 0;
|
||||
@@ -32,7 +35,12 @@ async function isUnmessagableGraph() {
|
||||
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
new Chart(ctx, {
|
||||
if (window.isUnmessagableChartInstance) {
|
||||
window.isUnmessagableChartInstance.destroy();
|
||||
window.isUnmessagableChartInstance = null;
|
||||
}
|
||||
|
||||
window.isUnmessagableChartInstance = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['unmessagable', 'messagable'],
|
||||
@@ -81,4 +89,11 @@ async function isUnmessagableGraph() {
|
||||
}
|
||||
}
|
||||
|
||||
isUnmessagableGraph();
|
||||
isUnmessagableGraph();
|
||||
|
||||
// Re-render on channel change
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (typeof StatusFilter !== 'undefined' && StatusFilter.subscribe) {
|
||||
StatusFilter.subscribe(() => isUnmessagableGraph());
|
||||
}
|
||||
});
|
||||
@@ -12,6 +12,8 @@ function getHopsColor(maxHops) {
|
||||
return 'rgb(200, 200, 200)';
|
||||
}
|
||||
|
||||
let maxHopsChartInstance = null;
|
||||
|
||||
async function maxHopsGraph() {
|
||||
const canvas = document.getElementById('maxHopsChart');
|
||||
const ctx = canvas.getContext('2d');
|
||||
@@ -29,7 +31,13 @@ async function maxHopsGraph() {
|
||||
const countsByHops = {};
|
||||
for (let i = 0; i <= 7; i++) countsByHops[i] = 0;
|
||||
|
||||
const nodesWithMaxHops = nodes.filter(n => n.max_hops !== null && n.max_hops !== undefined);
|
||||
const channelId = (typeof StatusFilter !== 'undefined' && StatusFilter.getChannelId) ? StatusFilter.getChannelId() : null;
|
||||
const filteredNodes = Array.isArray(nodes) ? nodes.filter(n => {
|
||||
if (!channelId) return true;
|
||||
return n.channel_id === channelId;
|
||||
}) : [];
|
||||
|
||||
const nodesWithMaxHops = filteredNodes.filter(n => n.max_hops !== null && n.max_hops !== undefined);
|
||||
|
||||
for (const node of nodesWithMaxHops) {
|
||||
const val = Number(node.max_hops);
|
||||
@@ -43,7 +51,12 @@ async function maxHopsGraph() {
|
||||
const chartContainer = document.getElementById('maxHopsContainer');
|
||||
if (chartContainer) chartContainer.style.height = `${labels.length * 35 + 50}px`;
|
||||
|
||||
new Chart(ctx, {
|
||||
if (maxHopsChartInstance) {
|
||||
maxHopsChartInstance.destroy();
|
||||
maxHopsChartInstance = null;
|
||||
}
|
||||
|
||||
maxHopsChartInstance = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels,
|
||||
@@ -75,3 +88,11 @@ async function maxHopsGraph() {
|
||||
maxHopsGraph();
|
||||
|
||||
|
||||
// Re-render on channel change
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (typeof StatusFilter !== 'undefined' && StatusFilter.subscribe) {
|
||||
StatusFilter.subscribe(() => maxHopsGraph());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -1,63 +1,94 @@
|
||||
async function mostActiveNodesGraph() {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const canvas = document.getElementById('mostActiveNodes');
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
let chartInstance = null;
|
||||
|
||||
// Show "Loading..." message
|
||||
ctx.font = '16px Arial';
|
||||
ctx.fillStyle = 'gray';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('Loading data...', canvas.width / 2, canvas.height / 2);
|
||||
|
||||
try {
|
||||
const response = await fetch('https://map.sthlm-mesh.se/api/v1/stats/most-active-nodes');
|
||||
const data = await response.json();
|
||||
|
||||
const labels = data.map(entry => entry.long_name);
|
||||
const counts = data.map(entry => entry.count);
|
||||
|
||||
// Adjust chart height based on number of items
|
||||
const chartContainer = document.getElementById('mostActiveNodesContainer');
|
||||
chartContainer.style.height = `${labels.length * 22 + 50}px`;
|
||||
|
||||
const backgroundColors = counts.map(count => {
|
||||
if (count < 100) return '#7EB26D'; // Green
|
||||
if (count < 200) return '#EAB839'; // Yellow
|
||||
return '#BF1B00'; // Red
|
||||
});
|
||||
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'Packages originated',
|
||||
data: counts,
|
||||
backgroundColor: backgroundColors,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
indexAxis: 'y', // horizontal bars
|
||||
scales: {
|
||||
y: {
|
||||
ticks: { autoSkip: false, font: { size: 12 } }
|
||||
},
|
||||
x: { max: 300 },
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.fillStyle = 'red';
|
||||
ctx.fillText('Error loading data', canvas.width / 2, canvas.height / 2);
|
||||
function getSelectedChannelId() {
|
||||
return (typeof StatusFilter !== 'undefined' && StatusFilter.getChannelId) ? StatusFilter.getChannelId() : null;
|
||||
}
|
||||
}
|
||||
|
||||
mostActiveNodesGraph();
|
||||
async function renderMostActiveNodes() {
|
||||
// Show "Loading..." message
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.font = '16px Arial';
|
||||
ctx.fillStyle = 'gray';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('Loading data...', canvas.width / 2, canvas.height / 2);
|
||||
|
||||
try {
|
||||
// Build URL with optional channel_id
|
||||
const baseUrl = 'https://map.sthlm-mesh.se/api/v1/stats/most-active-nodes';
|
||||
const channelId = getSelectedChannelId();
|
||||
const url = channelId ? `${baseUrl}?channel_id=${encodeURIComponent(channelId)}` : baseUrl;
|
||||
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
|
||||
const labels = data.map(entry => entry.long_name);
|
||||
const counts = data.map(entry => entry.count);
|
||||
|
||||
// Adjust chart height based on number of items
|
||||
const chartContainer = document.getElementById('mostActiveNodesContainer');
|
||||
if (chartContainer) {
|
||||
chartContainer.style.height = `${labels.length * 22 + 50}px`;
|
||||
}
|
||||
|
||||
const backgroundColors = counts.map(count => {
|
||||
if (count < 100) return '#7EB26D'; // Green
|
||||
if (count < 200) return '#EAB839'; // Yellow
|
||||
return '#BF1B00'; // Red
|
||||
});
|
||||
|
||||
if (chartInstance) {
|
||||
chartInstance.destroy();
|
||||
chartInstance = null;
|
||||
}
|
||||
|
||||
chartInstance = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'Packages originated',
|
||||
data: counts,
|
||||
backgroundColor: backgroundColors,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
indexAxis: 'y',
|
||||
scales: {
|
||||
y: {
|
||||
ticks: { autoSkip: false, font: { size: 12 } }
|
||||
},
|
||||
x: { max: 300 },
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
if (chartInstance) {
|
||||
chartInstance.destroy();
|
||||
chartInstance = null;
|
||||
}
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.fillStyle = 'red';
|
||||
ctx.fillText('Error loading data', canvas.width / 2, canvas.height / 2);
|
||||
}
|
||||
}
|
||||
|
||||
// Listen via shared StatusFilter
|
||||
if (typeof StatusFilter !== 'undefined' && StatusFilter.subscribe) {
|
||||
StatusFilter.subscribe(() => renderMostActiveNodes());
|
||||
}
|
||||
|
||||
// Initial render
|
||||
renderMostActiveNodes();
|
||||
});
|
||||
|
||||
+20
-1
@@ -1,7 +1,15 @@
|
||||
let selectedChannelIdNodes = null; // null = All
|
||||
|
||||
function countNodesSince(daysAgo) {
|
||||
const now = new Date();
|
||||
const threshold = new Date(now.getTime() - daysAgo * 24 * 60 * 60 * 1000);
|
||||
return nodes.filter(node => new Date(node.updated_at) >= threshold).length;
|
||||
return nodes
|
||||
.filter(node => new Date(node.updated_at) >= threshold)
|
||||
.filter(node => {
|
||||
if (!selectedChannelIdNodes) return true; // All
|
||||
return node.channel_id === selectedChannelIdNodes;
|
||||
})
|
||||
.length;
|
||||
}
|
||||
|
||||
async function nodesSeen() {
|
||||
@@ -13,3 +21,14 @@ async function nodesSeen() {
|
||||
}
|
||||
|
||||
nodesSeen();
|
||||
|
||||
// Listen for Antal Enheter channel changes (local only)
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (typeof StatusFilter !== 'undefined' && StatusFilter.subscribe) {
|
||||
selectedChannelIdNodes = StatusFilter.getChannelId ? StatusFilter.getChannelId() : null;
|
||||
StatusFilter.subscribe(() => {
|
||||
selectedChannelIdNodes = StatusFilter.getChannelId ? StatusFilter.getChannelId() : null;
|
||||
nodesSeen();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
let highlightedIndex = -1;
|
||||
let chartInstance;
|
||||
let selectedChannelIdPort = null; // null = All
|
||||
|
||||
|
||||
function filterSuggestions(query) {
|
||||
@@ -97,6 +98,21 @@ document.getElementById('clearFilterBtn').addEventListener('click', () => {
|
||||
portnumDistributionChart(); // Reload full chart
|
||||
});
|
||||
|
||||
// Channel filter listeners (mirrors messages/most-active-nodes design)
|
||||
if (typeof StatusFilter !== 'undefined' && StatusFilter.subscribe) {
|
||||
StatusFilter.subscribe(() => {
|
||||
selectedChannelIdPort = StatusFilter.getChannelId ? StatusFilter.getChannelId() : null;
|
||||
const currentNodeName = document.getElementById('nodeSearch').value.trim();
|
||||
if (currentNodeName) {
|
||||
const match = (nodes || []).find(n => n.long_name === currentNodeName);
|
||||
const nodeId = match ? match.node_id : null;
|
||||
portnumDistributionChart(nodeId);
|
||||
} else {
|
||||
portnumDistributionChart();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Main chart function
|
||||
async function portnumDistributionChart(nodeId = null) {
|
||||
const canvas = document.getElementById('portnumDistribution');
|
||||
@@ -110,9 +126,11 @@ async function portnumDistributionChart(nodeId = null) {
|
||||
|
||||
try {
|
||||
await fetchNodes();
|
||||
const url = nodeId
|
||||
? `https://map.sthlm-mesh.se/api/v1/stats/portnum-counts?nodeId=${nodeId}`
|
||||
: 'https://map.sthlm-mesh.se/api/v1/stats/portnum-counts';
|
||||
const base = 'https://map.sthlm-mesh.se/api/v1/stats/portnum-counts';
|
||||
const params = new URLSearchParams();
|
||||
if (nodeId) params.set('nodeId', nodeId);
|
||||
if (selectedChannelIdPort) params.set('channel_id', selectedChannelIdPort);
|
||||
const url = params.toString() ? `${base}?${params.toString()}` : base;
|
||||
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
|
||||
@@ -77,6 +77,8 @@ function formatPositionPrecision(positionPrecision) {
|
||||
|
||||
}
|
||||
|
||||
let positionPrecisionChartInstance = null;
|
||||
|
||||
async function positionPrecisionGraph() {
|
||||
const canvas = document.getElementById('positionPrecisionChart');
|
||||
const ctx = canvas.getContext('2d');
|
||||
@@ -90,10 +92,13 @@ async function positionPrecisionGraph() {
|
||||
|
||||
try {
|
||||
await fetchNodes();
|
||||
const channelId = (typeof StatusFilter !== 'undefined' && StatusFilter.getChannelId) ? StatusFilter.getChannelId() : null;
|
||||
const countsByPrecision = {};
|
||||
|
||||
// Filter out nodes without position_precision
|
||||
const nodesWithPoistion = nodes.filter(node => node.position_precision != null);
|
||||
const nodesWithPoistion = nodes
|
||||
.filter(node => node.position_precision != null)
|
||||
.filter(node => !channelId || node.channel_id === channelId);
|
||||
|
||||
// Count how many nodes fall into each precision value
|
||||
for (const node of nodesWithPoistion) {
|
||||
@@ -116,7 +121,12 @@ async function positionPrecisionGraph() {
|
||||
const chartContainer = document.getElementById('positionPrecisionContainer');
|
||||
chartContainer.style.height = `${labels.length * 35 + 50}px`;
|
||||
|
||||
new Chart(ctx, {
|
||||
if (positionPrecisionChartInstance) {
|
||||
positionPrecisionChartInstance.destroy();
|
||||
positionPrecisionChartInstance = null;
|
||||
}
|
||||
|
||||
positionPrecisionChartInstance = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
@@ -144,4 +154,11 @@ async function positionPrecisionGraph() {
|
||||
}
|
||||
|
||||
|
||||
positionPrecisionGraph();
|
||||
positionPrecisionGraph();
|
||||
|
||||
// Re-render on channel change
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (typeof StatusFilter !== 'undefined' && StatusFilter.subscribe) {
|
||||
StatusFilter.subscribe(() => positionPrecisionGraph());
|
||||
}
|
||||
});
|
||||
@@ -1,5 +1,20 @@
|
||||
let nodes = [];
|
||||
let fetchPromise = null;
|
||||
const StatusFilter = {
|
||||
_channelId: null,
|
||||
_subscribers: [],
|
||||
getChannelId() {
|
||||
return this._channelId;
|
||||
},
|
||||
subscribe(callback) {
|
||||
if (typeof callback === 'function') this._subscribers.push(callback);
|
||||
},
|
||||
_notify() {
|
||||
for (const cb of this._subscribers) {
|
||||
try { cb(this._channelId); } catch (_) {}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async function fetchNodes() {
|
||||
if (nodes.length > 0) return;
|
||||
@@ -19,3 +34,16 @@ async function fetchNodes() {
|
||||
|
||||
await fetchPromise;
|
||||
}
|
||||
|
||||
// Initialize global channel filter listeners once
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const radios = document.querySelectorAll("input[name='btnchannel-nodes']");
|
||||
if (!radios || radios.length === 0) return;
|
||||
const checked = document.querySelector("input[name='btnchannel-nodes']:checked");
|
||||
StatusFilter._channelId = checked ? (checked.getAttribute('data-channel') || null) : null;
|
||||
radios.forEach(r => r.addEventListener('change', () => {
|
||||
const channel = r.getAttribute('data-channel');
|
||||
StatusFilter._channelId = channel || null;
|
||||
StatusFilter._notify();
|
||||
}));
|
||||
});
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="yes"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml"><url><loc>https://sthlm-mesh.se/docs/settings/</loc><lastmod>2025-09-28T18:34:50+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/docs/device_role/</loc><lastmod>2025-09-28T18:34:50+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/docs/position/</loc><lastmod>2025-09-28T18:34:50+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/blog/2025/meetup-1-oktober-sundbyberg/</loc><lastmod>2025-09-24T19:44:30+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/blog/2025/%C3%B6verg%C3%A5ng-till-mediumfast-den-27-september/</loc><lastmod>2025-09-21T22:33:53+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/blog/2025/meshtastic-p%C3%A5-sec-t/</loc><lastmod>2025-09-21T20:27:22+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/blog/2025/meetup-19-augusti-telefonplan/</loc><lastmod>2025-09-21T20:27:22+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/blog/2025/meetup-8-april-sundbyberg/</loc><lastmod>2025-09-21T20:27:22+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/blog/2024/meetup-4-september-sundbyberg/</loc><lastmod>2025-09-21T20:27:22+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/blog/2024/meetup-15-maj-urban-deli/</loc><lastmod>2025-09-21T20:27:22+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/blog/</loc><lastmod>2025-08-24T19:47:51+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/docs/mqtt/</loc><lastmod>2025-03-31T19:16:34+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/blog/meetups/</loc><lastmod>2025-09-21T20:27:22+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/docs/neighbor_info/</loc><lastmod>2025-09-28T18:34:50+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/blog/past-meetups/</loc><lastmod>2025-09-21T20:27:22+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/docs/kartor/</loc><lastmod>2025-03-08T20:11:28+01:00</lastmod></url><url><loc>https://sthlm-mesh.se/docs/hardware/</loc><lastmod>2025-04-21T12:00:04+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/docs/solar_nodes/</loc><lastmod>2025-04-21T01:54:22+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/docs/communities/</loc><lastmod>2025-02-23T20:48:06+01:00</lastmod></url><url><loc>https://sthlm-mesh.se/categories/</loc></url><url><loc>https://sthlm-mesh.se/docs/</loc><lastmod>2025-03-03T22:48:04+01:00</lastmod></url><url><loc>https://sthlm-mesh.se/messages/</loc><lastmod>2025-09-28T21:23:33+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/about/</loc><lastmod>2025-02-23T18:02:23+01:00</lastmod></url><url><loc>https://sthlm-mesh.se/status/</loc><lastmod>2025-09-25T22:18:41+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/</loc><lastmod>2025-09-21T21:23:50+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/tags/</loc></url></urlset>
|
||||
<?xml version="1.0" encoding="utf-8" standalone="yes"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml"><url><loc>https://sthlm-mesh.se/docs/settings/</loc><lastmod>2025-09-28T18:34:50+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/docs/device_role/</loc><lastmod>2025-09-28T18:34:50+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/docs/position/</loc><lastmod>2025-09-28T18:34:50+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/blog/2025/meetup-1-oktober-sundbyberg/</loc><lastmod>2025-09-24T19:44:30+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/blog/2025/%C3%B6verg%C3%A5ng-till-mediumfast-den-27-september/</loc><lastmod>2025-09-21T22:33:53+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/blog/2025/meshtastic-p%C3%A5-sec-t/</loc><lastmod>2025-09-21T20:27:22+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/blog/2025/meetup-19-augusti-telefonplan/</loc><lastmod>2025-09-21T20:27:22+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/blog/2025/meetup-8-april-sundbyberg/</loc><lastmod>2025-09-21T20:27:22+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/blog/2024/meetup-4-september-sundbyberg/</loc><lastmod>2025-09-21T20:27:22+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/blog/2024/meetup-15-maj-urban-deli/</loc><lastmod>2025-09-21T20:27:22+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/blog/</loc><lastmod>2025-08-24T19:47:51+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/docs/mqtt/</loc><lastmod>2025-03-31T19:16:34+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/blog/meetups/</loc><lastmod>2025-09-21T20:27:22+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/docs/neighbor_info/</loc><lastmod>2025-09-28T18:34:50+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/blog/past-meetups/</loc><lastmod>2025-09-21T20:27:22+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/docs/kartor/</loc><lastmod>2025-03-08T20:11:28+01:00</lastmod></url><url><loc>https://sthlm-mesh.se/docs/hardware/</loc><lastmod>2025-04-21T12:00:04+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/docs/solar_nodes/</loc><lastmod>2025-04-21T01:54:22+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/docs/communities/</loc><lastmod>2025-02-23T20:48:06+01:00</lastmod></url><url><loc>https://sthlm-mesh.se/categories/</loc></url><url><loc>https://sthlm-mesh.se/docs/</loc><lastmod>2025-03-03T22:48:04+01:00</lastmod></url><url><loc>https://sthlm-mesh.se/messages/</loc><lastmod>2025-09-28T21:23:33+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/about/</loc><lastmod>2025-02-23T18:02:23+01:00</lastmod></url><url><loc>https://sthlm-mesh.se/status/</loc><lastmod>2025-10-05T22:29:26+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/</loc><lastmod>2025-09-21T21:23:50+02:00</lastmod></url><url><loc>https://sthlm-mesh.se/tags/</loc></url></urlset>
|
||||
+10
-18
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user