This commit is contained in:
2026-06-16 11:42:23 +03:00
parent 64607def4a
commit 0e1fa15a2f
3 changed files with 74 additions and 31 deletions
+1 -1
View File
@@ -379,7 +379,7 @@ def health():
return { return {
"ok": status["db_ok"], "ok": status["db_ok"],
"ts": time.time(), "ts": time.time(),
"api_build": "2026-06-16c", "api_build": "2026-06-16d",
**status, **status,
**elevation_status(), **elevation_status(),
} }
+69 -28
View File
@@ -361,7 +361,7 @@
{ position: 'topright', collapsed: true } { position: 'topright', collapsed: true }
).addTo(map); ).addTo(map);
const API_BUILD = '2026-06-16c'; const API_BUILD = '2026-06-16d';
const markers = {}; const markers = {};
let selectedId = null; let selectedId = null;
@@ -560,10 +560,13 @@
} }
function snapAtCursor(track, telemetryRows, cursor, roleFallback) { function snapAtCursor(track, telemetryRows, cursor, roleFallback) {
const pos = trackPointAt(track, cursor);
if (pos?.meta && String(pos.meta).length > 2) {
const ts = timelineUseProgress ? null : cursor.t;
return snapFromTrackPoint(pos, ts, roleFallback || track?.role);
}
if (timelineUseProgress) { if (timelineUseProgress) {
const pos = positionAtProgress(track?.points, cursor.progress); return snapFromTrackPoint(pos, null, roleFallback || track?.role);
if (!pos) return null;
return { meta: pos.meta, role: roleFallback, rssi: pos.rssi, ts: cursor.progress };
} }
return snapAtTime(track, telemetryRows, cursor.t, roleFallback); return snapAtTime(track, telemetryRows, cursor.t, roleFallback);
} }
@@ -592,21 +595,42 @@
return { ...track, points }; return { ...track, points };
} }
function snapAtTime(track, telemetryRows, t, roleFallback) { function normalizeTelemetry(rows) {
const tel = telemetryAtTime(telemetryRows, t); if (!rows?.length) return [];
if (tel) return tel; return rows
const pos = positionAt(track?.points, t); .map(r => ({ ...r, ts: Number(r.ts) }))
.sort((a, b) => a.ts - b.ts);
}
function trackPointAt(track, cursor) {
if (!track?.points?.length) return null;
return timelineUseProgress
? positionAtProgress(track.points, cursor.progress)
: positionAt(track.points, cursor.t);
}
function snapFromTrackPoint(pos, t, roleFallback) {
if (!pos) return null; if (!pos) return null;
return { return {
meta: pos.meta, meta: pos.meta,
role: roleFallback || track?.role, role: roleFallback,
rssi: pos.rssi, rssi: pos.rssi,
ts: t, ts: t,
lat: pos.lat, lat: pos.lat,
lon: pos.lon lon: pos.lon,
}; };
} }
function snapAtTime(track, telemetryRows, t, roleFallback) {
const pos = positionAt(track?.points, t);
if (pos?.meta && String(pos.meta).length > 2) {
return snapFromTrackPoint(pos, t, roleFallback || track?.role);
}
const tel = telemetryAtTime(telemetryRows, t);
if (tel) return tel;
return snapFromTrackPoint(pos, t, roleFallback || track?.role);
}
function rxQualityFromMeta(meta) { function rxQualityFromMeta(meta) {
if (!meta) return null; if (!meta) return null;
const snap = RadioUI.parseRadioSnapshot(meta); const snap = RadioUI.parseRadioSnapshot(meta);
@@ -1847,18 +1871,17 @@
function telemetryAtTime(rows, t) { function telemetryAtTime(rows, t) {
if (!rows?.length) return null; if (!rows?.length) return null;
const first = rows[0]; const tNum = Number(t);
const last = rows[rows.length - 1]; let best = null;
if (t <= first.ts) return first; let bestD = Infinity;
if (t >= last.ts) return last; for (const r of rows) {
for (let i = 0; i < rows.length - 1; i++) { const d = Math.abs(Number(r.ts) - tNum);
const a = rows[i]; if (d < bestD) {
const b = rows[i + 1]; best = r;
if (t >= a.ts && t <= b.ts) { bestD = d;
return t - a.ts <= b.ts - t ? a : b;
} }
} }
return last; return best;
} }
function telemetryFromTrackPoint(track, t, roleFallback) { function telemetryFromTrackPoint(track, t, roleFallback) {
@@ -1890,9 +1913,21 @@
return bestD === 0 ? best : null; return bestD === 0 ? best : null;
} }
function trackMetaDiversity(points) {
const packets = new Set();
let withMeta = 0;
for (const p of points || []) {
if (!p.meta || String(p.meta).length < 3) continue;
withMeta++;
const snap = RadioUI.parseRadioSnapshot(p.meta);
if (snap.packet != null) packets.add(snap.packet);
}
return { withMeta, uniquePackets: packets.size, total: points?.length || 0 };
}
function pairedTelemetryAtTime(txTrack, rxTrack, telemetryTx, telemetryRx, t) { function pairedTelemetryAtTime(txTrack, rxTrack, telemetryTx, telemetryRx, t) {
let txTel = telemetryAtTime(telemetryTx, t) || telemetryFromTrackPoint(txTrack, t, 'TX'); let txTel = snapAtTime(txTrack, telemetryTx, t, 'TX');
let rxTel = telemetryAtTime(telemetryRx, t) || telemetryFromTrackPoint(rxTrack, t, 'RX'); let rxTel = snapAtTime(rxTrack, telemetryRx, t, 'RX');
const txSnap = txTel ? telemetryToSnap(txTel) : null; const txSnap = txTel ? telemetryToSnap(txTel) : null;
const rxSnap = rxTel ? telemetryToSnap(rxTel) : null; const rxSnap = rxTel ? telemetryToSnap(rxTel) : null;
if (txSnap?.packet != null) { if (txSnap?.packet != null) {
@@ -2211,6 +2246,12 @@
noteText = noteText =
'Треки не пересекаются по времени — шкала на полном диапазоне; вне записи позиция удерживается на краю.'; 'Треки не пересекаются по времени — шкала на полном диапазоне; вне записи позиция удерживается на краю.';
} }
const txDiv = trackMetaDiversity(loadedTxTrack.points);
const rxDiv = trackMetaDiversity(loadedRxTrack.points);
if (txDiv.uniquePackets <= 1 && rxDiv.uniquePackets <= 1
&& (txDiv.withMeta > 1 || rxDiv.withMeta > 1)) {
noteText += ' Радио-статистика в точках трека не менялась при записи.';
}
applyTimelineRange(range, noteText); applyTimelineRange(range, noteText);
} }
setTimelineVisible(true); setTimelineVisible(true);
@@ -2227,7 +2268,7 @@
`/api/telemetry?device_id=${encodeURIComponent(loadedSingleTrack.device_id)}&since=${range.min}&until=${range.max}&limit=500`, `/api/telemetry?device_id=${encodeURIComponent(loadedSingleTrack.device_id)}&since=${range.min}&until=${range.max}&limit=500`,
{ cache: 'no-store' } { cache: 'no-store' }
); );
if (res.ok) telemetrySingle = await res.json(); if (res.ok) telemetrySingle = normalizeTelemetry(await res.json());
} }
updateTimelineAtSingle(timelineCursor()); updateTimelineAtSingle(timelineCursor());
return; return;
@@ -2240,8 +2281,8 @@
fetch(`/api/telemetry?device_id=${encodeURIComponent(loadedTxTrack.device_id)}&since=${range.min}&until=${range.max}&limit=500`, { cache: 'no-store' }), fetch(`/api/telemetry?device_id=${encodeURIComponent(loadedTxTrack.device_id)}&since=${range.min}&until=${range.max}&limit=500`, { cache: 'no-store' }),
fetch(`/api/telemetry?device_id=${encodeURIComponent(loadedRxTrack.device_id)}&since=${range.min}&until=${range.max}&limit=500`, { cache: 'no-store' }) fetch(`/api/telemetry?device_id=${encodeURIComponent(loadedRxTrack.device_id)}&since=${range.min}&until=${range.max}&limit=500`, { cache: 'no-store' })
]); ]);
if (telTx.ok) telemetryTx = await telTx.json(); if (telTx.ok) telemetryTx = normalizeTelemetry(await telTx.json());
if (telRx.ok) telemetryRx = await telRx.json(); if (telRx.ok) telemetryRx = normalizeTelemetry(await telRx.json());
} }
updateTimelineAt(timelineCursor()); updateTimelineAt(timelineCursor());
} }
@@ -2326,7 +2367,7 @@
`/api/telemetry?device_id=${encodeURIComponent(loadedSingleTrack.device_id)}&since=${range.min}&until=${range.max}&limit=500`, `/api/telemetry?device_id=${encodeURIComponent(loadedSingleTrack.device_id)}&since=${range.min}&until=${range.max}&limit=500`,
{ cache: 'no-store' } { cache: 'no-store' }
); );
if (telRes.ok) telemetrySingle = await telRes.json(); if (telRes.ok) telemetrySingle = normalizeTelemetry(await telRes.json());
updateTimelineAtSingle(timelineCursor()); updateTimelineAtSingle(timelineCursor());
} }
document.getElementById('trackInfo').textContent = document.getElementById('trackInfo').textContent =
@@ -2387,8 +2428,8 @@
fetch(`/api/telemetry?device_id=${encodeURIComponent(loadedTxTrack.device_id)}&since=${range.min}&until=${range.max}&limit=500`, { cache: 'no-store' }), fetch(`/api/telemetry?device_id=${encodeURIComponent(loadedTxTrack.device_id)}&since=${range.min}&until=${range.max}&limit=500`, { cache: 'no-store' }),
fetch(`/api/telemetry?device_id=${encodeURIComponent(loadedRxTrack.device_id)}&since=${range.min}&until=${range.max}&limit=500`, { cache: 'no-store' }) fetch(`/api/telemetry?device_id=${encodeURIComponent(loadedRxTrack.device_id)}&since=${range.min}&until=${range.max}&limit=500`, { cache: 'no-store' })
]); ]);
if (telTx.ok) telemetryTx = await telTx.json(); if (telTx.ok) telemetryTx = normalizeTelemetry(await telTx.json());
if (telRx.ok) telemetryRx = await telRx.json(); if (telRx.ok) telemetryRx = normalizeTelemetry(await telRx.json());
updateTimelineAt(timelineCursor()); updateTimelineAt(timelineCursor());
} }
+4 -2
View File
@@ -116,8 +116,10 @@
function renderCompareGrid(txSnap, rxSnap, txId, rxId, changedTx, changedRx, staticOpen) { function renderCompareGrid(txSnap, rxSnap, txId, rxId, changedTx, changedRx, staticOpen) {
let html = '<div class="radio-compare-grid">'; let html = '<div class="radio-compare-grid">';
html += `<div class="radio-compare-head"><span class="legend-tx">TX</span> ${escapeHtml(txId || '—')}`; html += '<div class="radio-compare-head">';
html += `<span class="legend-rx">RX</span> ${escapeHtml(rxId || '—')}</div>`; html += `<span><span class="legend-tx">TX</span> ${escapeHtml(txId || '—')}</span>`;
html += `<span><span class="legend-rx">RX</span> ${escapeHtml(rxId || '—')}</span>`;
html += '</div>';
for (const row of DYNAMIC_ROWS) { for (const row of DYNAMIC_ROWS) {
const txCls = changedTx && changedTx.has(row.key) ? ' changed' : ''; const txCls = changedTx && changedTx.has(row.key) ? ' changed' : '';
const rxCls = changedRx && changedRx.has(row.key) ? ' changed' : ''; const rxCls = changedRx && changedRx.has(row.key) ? ' changed' : '';