This commit is contained in:
Roslund
2025-10-05 20:30:12 +00:00
parent c0860124cb
commit 2ce2871da8
13 changed files with 316 additions and 113 deletions
+23 -15
View File
@@ -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>
&lt;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 1520% ä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.&lt;/p></description></item></channel></rss>
&lt;p class="text-muted">Senaste 7 dagarna&lt;/p></description></item></channel></rss>
+26 -2
View File
@@ -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();
});
}
});
+18 -3
View File
@@ -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());
}
});
+18 -3
View File
@@ -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());
}
});
+19 -4
View File
@@ -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());
}
});
+23 -2
View File
@@ -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());
}
});
+89 -58
View File
@@ -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
View File
@@ -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();
});
}
});
+21 -3
View File
@@ -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();
+20 -3
View File
@@ -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());
}
});
+28
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because one or more lines are too long