Improve live node positions and expose precision metadata (#231)

* Fetch latest node positions and precision metadata

* Stop showing position source and precision in UI

* Guard node positions against stale merges
This commit is contained in:
l5y
2025-10-05 23:08:57 +02:00
committed by GitHub
parent a3fb9b0d5c
commit 09a2d849ec
5 changed files with 706 additions and 219 deletions
+89 -4
View File
@@ -1813,12 +1813,88 @@ var(--fg); }
return r.json();
}
async function fetchPositions(limit = NODE_LIMIT) {
const r = await fetch(`/api/positions?limit=${limit}`, { cache: 'no-store' });
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
}
function toFiniteNumber(value) {
if (value == null || value === '') return null;
const num = typeof value === 'number' ? value : Number(value);
return Number.isFinite(num) ? num : null;
}
function resolveTimestampSeconds(numeric, isoString) {
const parsedNumeric = toFiniteNumber(numeric);
if (parsedNumeric != null) return parsedNumeric;
if (typeof isoString === 'string' && isoString.length) {
const parsedIso = Date.parse(isoString);
if (Number.isFinite(parsedIso)) {
return parsedIso / 1000;
}
}
return null;
}
function mergePositionsIntoNodes(nodes, positions) {
if (!Array.isArray(nodes) || !Array.isArray(positions) || nodes.length === 0) return;
const nodesById = new Map();
for (const node of nodes) {
if (!node || typeof node !== 'object') continue;
const key = typeof node.node_id === 'string' ? node.node_id : null;
if (key) nodesById.set(key, node);
}
if (nodesById.size === 0) return;
const updated = new Set();
for (const pos of positions) {
if (!pos || typeof pos !== 'object') continue;
const nodeId = typeof pos.node_id === 'string' ? pos.node_id : null;
if (!nodeId || updated.has(nodeId)) continue;
const node = nodesById.get(nodeId);
if (!node) continue;
const lat = toFiniteNumber(pos.latitude);
const lon = toFiniteNumber(pos.longitude);
if (lat == null || lon == null) continue;
const currentTimestamp = resolveTimestampSeconds(node.position_time, node.pos_time_iso);
const incomingTimestamp = resolveTimestampSeconds(pos.position_time, pos.position_time_iso);
if (currentTimestamp != null) {
if (incomingTimestamp == null || incomingTimestamp <= currentTimestamp) {
continue;
}
}
updated.add(nodeId);
node.latitude = lat;
node.longitude = lon;
const alt = toFiniteNumber(pos.altitude);
if (alt != null) node.altitude = alt;
const posTime = toFiniteNumber(pos.position_time);
if (posTime != null) {
node.position_time = posTime;
node.pos_time_iso = typeof pos.position_time_iso === 'string' && pos.position_time_iso.length
? pos.position_time_iso
: new Date(posTime * 1000).toISOString();
} else if (typeof pos.position_time_iso === 'string' && pos.position_time_iso.length) {
node.pos_time_iso = pos.position_time_iso;
}
if (pos.location_source != null && pos.location_source !== '') {
node.location_source = pos.location_source;
}
const precision = toFiniteNumber(pos.precision_bits);
if (precision != null) node.precision_bits = precision;
}
}
function buildTelemetryIndex(entries) {
const byNodeId = new Map();
const byNodeNum = new Map();
@@ -1938,6 +2014,8 @@ var(--fg); }
const frag = document.createDocumentFragment();
for (const n of nodes) {
const tr = document.createElement('tr');
const lastPositionTime = toFiniteNumber(n.position_time ?? n.positionTime);
const lastPositionCell = lastPositionTime != null ? timeAgo(lastPositionTime, nowSec) : '';
tr.innerHTML = `
<td class="mono">${n.node_id || ""}</td>
<td>${renderShortHtml(n.short_name, n.role, n.long_name, n)}</td>
@@ -1956,7 +2034,7 @@ var(--fg); }
<td>${fmtCoords(n.latitude)}</td>
<td>${fmtCoords(n.longitude)}</td>
<td>${fmtAlt(n.altitude, "m")}</td>
<td class="mono">${n.pos_time_iso ? `${timeAgo(n.position_time, nowSec)}` : ""}</td>`;
<td class="mono">${lastPositionCell}</td>`;
frag.appendChild(tr);
}
tb.replaceChildren(frag);
@@ -2112,8 +2190,9 @@ var(--fg); }
if (n.last_heard) {
lines.push(`Last seen: ${timeAgo(n.last_heard, nowSec)}`);
}
if (n.pos_time_iso) {
lines.push(`Last Position: ${timeAgo(n.position_time, nowSec)}`);
const lastPositionTime = toFiniteNumber(n.position_time ?? n.positionTime);
if (lastPositionTime != null) {
lines.push(`Last Position: ${timeAgo(lastPositionTime, nowSec)}`);
}
if (n.uptime_seconds) {
lines.push(`Uptime: ${timeHum(n.uptime_seconds)}`);
@@ -2208,13 +2287,19 @@ var(--fg); }
console.warn('telemetry refresh failed; continuing without telemetry', err);
return [];
});
const [nodes, neighborTuples, messages, telemetryEntries] = await Promise.all([
const positionsPromise = fetchPositions().catch(err => {
console.warn('position refresh failed; continuing without updates', err);
return [];
});
const [nodes, positions, neighborTuples, messages, telemetryEntries] = await Promise.all([
fetchNodes(),
positionsPromise,
neighborPromise,
fetchMessages(),
telemetryPromise,
]);
nodes.forEach(applyNodeNameFallback);
mergePositionsIntoNodes(nodes, positions);
computeDistances(nodes);
mergeTelemetryIntoNodes(nodes, telemetryEntries);
if (Array.isArray(messages)) {