web: display sats in view (#523)

This commit is contained in:
l5y
2025-11-26 22:01:53 +01:00
committed by GitHub
parent f4aee2aba4
commit 7160d72aae
8 changed files with 281 additions and 0 deletions
+79
View File
@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:ns1="http://sozi.baierouge.fr"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:dc="http://purl.org/dc/elements/1.1/"
id="svg7158"
viewBox="0 0 453.54 453.54"
version="1.1"
>
<title
id="title7193"
>Satellite Icon</title
>
<g
id="layer1"
transform="translate(-174.99 -97.13)"
>
<path
id="path7145"
style="fill:#000000"
d="m287.44 286.58c8.8583-0.0756 18.161 1.2414 27.843 4.3505 23.639 7.5916 49.437 23.77 72.544 46.876 22.316 22.316 38.195 47.044 46.114 70.042 3.5848 10.411 5.3257 20.665 5.3294 30.127 34.464-34.381 74.719-74.501 74.719-74.501 8.8046-8.8046 10.662-25.016 4.0244-45.788-6.6374-20.772-21.622-44.897-43.287-66.562-21.664-21.665-45.789-36.649-66.561-43.287-20.772-6.6373-36.984-4.7803-45.788 4.0241a7.3368 7.3368 0 0 1 -0.32655 0.32655c0.00015-0.00038-0.15572-0.0378-0.32656 0.10658a7.3368 7.3368 0 0 1 -0.10733 0.10809c-0.0755 0.0755-0.23434 0.23735-0.3258 0.32655a7.3368 7.3368 0 0 1 -0.10734 0.10734c-0.0227 0.0227-0.0755 0.0907-0.10734 0.10734-0.27741 0.27062-0.46261 0.46942-0.87004 0.87005-1.3246 1.3035-3.2358 3.2497-5.7641 5.7642-5.0603 5.0256-12.407 12.42-21.317 21.317-11.973 11.959-29.332 29.327-45.679 45.68zm-40.639-122.78-33.338-33.338 33.338-33.338 33.338 33.338zm38.467 38.467-33.338-33.338 33.338-33.338 33.338 33.338zm38.467 38.467-33.338-33.338 33.338-33.338 33.338 33.338zm-115.4-38.467-33.338-33.338 33.338-33.338 33.338 33.338zm38.467 38.467-33.338-33.338 33.338-33.338 33.338 33.338zm38.467 38.467-33.338-33.338 33.338-33.338 33.338 33.338zm233 156.07-33.338-33.338 33.338-33.338 33.338 33.338zm38.467 38.467-33.338-33.338 33.338-33.338 33.338 33.338zm38.467 38.467-33.338-33.338 33.338-33.338 33.338 33.338zm-115.4-38.467-33.338-33.338 33.338-33.338 33.338 33.338zm38.467 38.467-33.338-33.338 33.338-33.338 33.338 33.338zm38.467 38.467-33.338-33.338 33.338-33.338 33.338 33.338zm-26.89-249.86c14.671-18.593 23.601-32.737 23.601-32.737 6.0975-9.8498 8.7897-20.878 6.5255-33.498-2.2639-12.621-9.7854-27.187-25.667-43.069-15.989-15.989-29.977-23.759-42.199-26.103-12.222-2.3439-23.154 0.29026-34.586 6.9608a8.7573 8.7573 0 0 1 -0.10733 0.10734c-0.90581 0.51174-0.23131 0.0975-0.4354 0.21694-0.20486 0.11566-0.55106 0.2948-0.97959 0.54275-0.85462 0.49889-2.117 1.2198-3.8063 2.284-3.3786 2.1287-8.3084 5.4327-14.574 10.006-3.793 2.7685-8.1932 6.2168-12.943 10.115 20.824 8.7749 42.483 23.558 62.211 43.287 19.646 19.646 34.196 41.165 42.96 61.885zm-243.08 138.23c15.825 15.825 27.262 20.215 35.673 20.012 8.2898-0.19956 16.009-4.9974 24.362-13.269 0.18141-0.20258 0.17082-0.17235 0.32654-0.32579 0.67351-0.65991 1.1912-1.0953 1.5228-1.3049 0.81396-0.51552 1.1331-0.63722 1.4148-0.75877 0.5639-0.24415 0.87103-0.43313 1.196-0.54349 0.65234-0.22073 1.3406-0.40517 2.1753-0.65311 1.67-0.49361 3.9356-1.1132 6.6343-1.8491 5.3972-1.4715 12.536-3.2413 19.686-5.1116 12.5-3.2709 22.067-5.7697 25.015-6.5254 5.2003-6.2337 6.9486-16.641 2.8278-30.888-4.3795-15.141-14.954-33.009-30.779-48.834s-33.693-26.4-48.834-30.779c-14.246-4.121-24.654-2.3723-30.888 2.8278-0.39307 1.4287-0.92984 3.0175-1.5226 5.2205-1.4259 5.3024-3.3261 12.432-5.2202 19.577-3.793 14.289-7.613 28.713-7.613 28.713a9.2981 9.2981 0 0 1 -2.3919 4.1331c-8.4974 8.4974-13.392 16.277-13.595 24.689-0.20259 8.4119 4.1869 19.848 20.012 35.673z"
/>
</g
>
<metadata
>
<rdf:RDF
>
<cc:Work
>
<dc:format
>image/svg+xml</dc:format
>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage"
/>
<cc:license
rdf:resource="http://creativecommons.org/licenses/publicdomain/"
/>
<dc:publisher
>
<cc:Agent
rdf:about="http://openclipart.org/"
>
<dc:title
>Openclipart</dc:title
>
</cc:Agent
>
</dc:publisher
>
</cc:Work
>
<cc:License
rdf:about="http://creativecommons.org/licenses/publicdomain/"
>
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction"
/>
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution"
/>
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks"
/>
</cc:License
>
</rdf:RDF
>
</metadata
>
</svg
>

After

Width:  |  Height:  |  Size: 4.7 KiB

@@ -113,6 +113,36 @@ test('refreshNodeInformation merges telemetry metrics when the base node lacks t
});
});
test('refreshNodeInformation surfaces sats_in_view from the latest position packet', async () => {
const calls = [];
const responses = new Map([
['/api/nodes/!sat?limit=7', createResponse(200, {
node_id: '!sat',
short_name: 'SAT',
})],
['/api/telemetry/!sat?limit=1000', createResponse(404, { error: 'not found' })],
['/api/positions/!sat?limit=7', createResponse(200, [
{ node_id: '!sat', position_time: 200, rx_time: 200, latitude: 1.1, longitude: 2.1, sats_in_view: 8 },
{ node_id: '!sat', position_time: 100, rx_time: 100, latitude: 1, longitude: 2, sats_in_view: 3 },
])],
['/api/neighbors/!sat?limit=1000', createResponse(404, { error: 'not found' })],
]);
const fetchImpl = async (url, options) => {
calls.push({ url, options });
return responses.get(url) ?? createResponse(404, { error: 'not found' });
};
const node = await refreshNodeInformation({ nodeId: '!sat' }, { fetchImpl });
assert.equal(node.satsInView, 8);
assert.equal(node.sats_in_view, 8);
assert.ok(node.position);
assert.equal(node.position.sats_in_view, 8);
assert.equal(node.rawSources.position.sats_in_view, 8);
assert.equal(calls.length, 4);
});
test('refreshNodeInformation normalizes telemetry aliases for downstream consumers', async () => {
const responses = new Map([
['/api/nodes/!chan?limit=7', createResponse(404, { error: 'not found' })],
@@ -0,0 +1,47 @@
/*
* Copyright © 2025-26 l5yth & contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import test from 'node:test';
import assert from 'node:assert/strict';
import { renderSatsInViewBadge, resolveSatsInView, __testUtils } from '../short-info-satellites.js';
const { toPositiveInteger } = __testUtils;
test('resolveSatsInView inspects aliases and nested payloads', () => {
assert.equal(resolveSatsInView({ sats_in_view: '3.6' }), 4);
assert.equal(resolveSatsInView({ position: { satsInView: 5 } }), 5);
assert.equal(resolveSatsInView({ rawSources: { position: { sats_in_view: 9 } } }), 9);
assert.equal(resolveSatsInView({ satsInView: 0 }), null);
assert.equal(resolveSatsInView(null), null);
});
test('renderSatsInViewBadge returns markup only for positive counts', () => {
const html = renderSatsInViewBadge({ satsInView: 6 });
assert.match(html, /short-info-sats/);
assert.ok(html.includes('satellite-icon.svg'));
assert.match(html, />6</);
assert.equal(renderSatsInViewBadge({ satsInView: 0 }), '');
assert.equal(renderSatsInViewBadge({ position: { sats_in_view: -1 } }), '');
});
test('toPositiveInteger normalizes numeric values defensively', () => {
assert.equal(toPositiveInteger('7.2'), 7);
assert.equal(toPositiveInteger(''), null);
assert.equal(toPositiveInteger(-3), null);
assert.equal(toPositiveInteger(NaN), null);
});
+6
View File
@@ -34,6 +34,7 @@ import {
fmtTemperature,
fmtTx,
} from './short-info-telemetry.js';
import { renderSatsInViewBadge } from './short-info-satellites.js';
import { createMessageNodeHydrator } from './message-node-hydrator.js';
import {
extractChatMessageMetadata,
@@ -2002,6 +2003,7 @@ export function initializeApp(config) {
['latitude', source.latitude],
['longitude', source.longitude],
['altitude', source.altitude],
['satsInView', source.satsInView ?? source.sats_in_view],
['positionTime', source.positionTime ?? source.position_time],
];
for (const [key, value] of numericPairs) {
@@ -2116,6 +2118,10 @@ export function initializeApp(config) {
if (nodeIdValue !== '—') {
shortParts.push(`<span class="mono">${escapeHtml(nodeIdValue)}</span>`);
}
const satelliteLine = renderSatsInViewBadge(overlayInfo);
if (satelliteLine) {
shortParts.push(satelliteLine);
}
if (shortParts.length) {
lines.push(shortParts.join(' '));
}
+3
View File
@@ -181,6 +181,7 @@ function mergeNodeFields(target, record) {
assignString(target, 'hwModel', extractString(record, ['hwModel', 'hw_model']));
mergeModemMetadata(target, record);
assignNumber(target, 'snr', extractNumber(record, ['snr']));
assignNumber(target, 'satsInView', extractNumber(record, ['sats_in_view', 'satsInView']));
assignNumber(target, 'battery', extractNumber(record, ['battery', 'battery_level', 'batteryLevel']));
assignNumber(target, 'voltage', extractNumber(record, ['voltage']));
assignNumber(target, 'uptime', extractNumber(record, ['uptime', 'uptime_seconds', 'uptimeSeconds']));
@@ -278,6 +279,8 @@ function mergePosition(target, position) {
assignString(target, 'lastSeenIso', extractString(position, ['rx_iso', 'rxIso']), { preferExisting: true });
}
}
assignNumber(target, 'satsInView', extractNumber(position, ['sats_in_view', 'satsInView']), { preferExisting: true });
}
/**
@@ -74,6 +74,7 @@ const FIELD_ALIASES = Object.freeze([
{ keys: ['relative_humidity', 'relativeHumidity', 'humidity'], normalise: normalizeNumber },
{ keys: ['barometric_pressure', 'barometricPressure', 'pressure'], normalise: normalizeNumber },
{ keys: ['gas_resistance', 'gasResistance'], normalise: normalizeNumber },
{ keys: ['sats_in_view', 'satsInView'], normalise: normalizeNumber },
{ keys: ['snr'], normalise: normalizeNumber },
{ keys: ['last_heard', 'lastHeard'], normalise: normalizeNumber },
{ keys: ['last_seen_iso', 'lastSeenIso'], normalise: normalizeString },
@@ -0,0 +1,82 @@
/*
* Copyright © 2025-26 l5yth & contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Coerce a candidate value into a positive integer satellite count.
*
* @param {*} value Raw candidate value.
* @returns {number|null} Rounded positive integer or ``null``.
*/
function toPositiveInteger(value) {
if (value == null || value === '') return null;
const num = typeof value === 'number' ? value : Number(value);
if (!Number.isFinite(num) || num <= 0) return null;
return Math.round(num);
}
/**
* Extract the satellite count from a node-like payload.
*
* @param {*} info Node payload potentially containing satellite metadata.
* @returns {number|null} Satellite count when present and positive.
*/
export function resolveSatsInView(info) {
if (!info || typeof info !== 'object') {
return toPositiveInteger(info);
}
const candidates = [
info.satsInView,
info.sats_in_view,
info.position?.satsInView,
info.position?.sats_in_view,
info.rawSources?.position?.satsInView,
info.rawSources?.position?.sats_in_view,
];
for (const candidate of candidates) {
const count = toPositiveInteger(candidate);
if (count != null) {
return count;
}
}
return null;
}
const ICON_PATH = '/assets/img/satellite-icon.svg';
/**
* Render a short-info overlay row describing visible satellites.
*
* @param {*} info Node payload providing satellite metadata.
* @returns {string} HTML snippet or an empty string when unavailable.
*/
export function renderSatsInViewBadge(info) {
const count = resolveSatsInView(info);
if (count == null) {
return '';
}
return [
'<span class="short-info-sats" aria-label="Satellites in view">',
`<span class="short-info-sats__icon" aria-hidden="true">` +
`<img src="${ICON_PATH}" alt="" width="14" height="14" loading="lazy" decoding="async" class="short-info-sats__glyph">` +
`</span>`,
`<span class="short-info-sats__count">${count}</span>`,
'</span>',
].join('');
}
export const __testUtils = {
toPositiveInteger,
};
+33
View File
@@ -783,6 +783,39 @@ body.view-map .map-panel--full #map {
background: rgba(0, 0, 0, 0.08);
}
.short-info-sats {
display: inline-flex;
align-items: center;
gap: 4px;
font-weight: inherit;
color: inherit;
padding: 0;
margin: 0;
vertical-align: middle;
}
.short-info-sats__icon {
display: inline-flex;
align-items: center;
justify-content: center;
color: currentColor;
}
.short-info-sats__glyph {
display: block;
filter: grayscale(1);
}
body.dark .short-info-sats__glyph {
filter: invert(1) grayscale(1);
}
.short-info-sats__count {
font-family: inherit;
font-size: inherit;
letter-spacing: 0.1px;
}
.node-detail-overlay {
position: fixed;
inset: 0;