mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-08-07 01:13:01 +02:00
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:
+89
-4
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user