diff --git a/server/fastapi_app.py b/server/fastapi_app.py index 78609b7..ba73e8c 100644 --- a/server/fastapi_app.py +++ b/server/fastapi_app.py @@ -379,7 +379,7 @@ def health(): return { "ok": status["db_ok"], "ts": time.time(), - "api_build": "2026-06-16c", + "api_build": "2026-06-16d", **status, **elevation_status(), } diff --git a/server/static/index.html b/server/static/index.html index e793217..c210b9e 100644 --- a/server/static/index.html +++ b/server/static/index.html @@ -361,7 +361,7 @@ { position: 'topright', collapsed: true } ).addTo(map); - const API_BUILD = '2026-06-16c'; + const API_BUILD = '2026-06-16d'; const markers = {}; let selectedId = null; @@ -560,10 +560,13 @@ } 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) { - const pos = positionAtProgress(track?.points, cursor.progress); - if (!pos) return null; - return { meta: pos.meta, role: roleFallback, rssi: pos.rssi, ts: cursor.progress }; + return snapFromTrackPoint(pos, null, roleFallback || track?.role); } return snapAtTime(track, telemetryRows, cursor.t, roleFallback); } @@ -592,21 +595,42 @@ return { ...track, points }; } - function snapAtTime(track, telemetryRows, t, roleFallback) { - const tel = telemetryAtTime(telemetryRows, t); - if (tel) return tel; - const pos = positionAt(track?.points, t); + function normalizeTelemetry(rows) { + if (!rows?.length) return []; + return rows + .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; return { meta: pos.meta, - role: roleFallback || track?.role, + role: roleFallback, rssi: pos.rssi, ts: t, 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) { if (!meta) return null; const snap = RadioUI.parseRadioSnapshot(meta); @@ -1847,18 +1871,17 @@ function telemetryAtTime(rows, t) { if (!rows?.length) return null; - const first = rows[0]; - const last = rows[rows.length - 1]; - if (t <= first.ts) return first; - if (t >= last.ts) return last; - for (let i = 0; i < rows.length - 1; i++) { - const a = rows[i]; - const b = rows[i + 1]; - if (t >= a.ts && t <= b.ts) { - return t - a.ts <= b.ts - t ? a : b; + const tNum = Number(t); + let best = null; + let bestD = Infinity; + for (const r of rows) { + const d = Math.abs(Number(r.ts) - tNum); + if (d < bestD) { + best = r; + bestD = d; } } - return last; + return best; } function telemetryFromTrackPoint(track, t, roleFallback) { @@ -1890,9 +1913,21 @@ 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) { - let txTel = telemetryAtTime(telemetryTx, t) || telemetryFromTrackPoint(txTrack, t, 'TX'); - let rxTel = telemetryAtTime(telemetryRx, t) || telemetryFromTrackPoint(rxTrack, t, 'RX'); + let txTel = snapAtTime(txTrack, telemetryTx, t, 'TX'); + let rxTel = snapAtTime(rxTrack, telemetryRx, t, 'RX'); const txSnap = txTel ? telemetryToSnap(txTel) : null; const rxSnap = rxTel ? telemetryToSnap(rxTel) : null; if (txSnap?.packet != null) { @@ -2211,6 +2246,12 @@ 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); } setTimelineVisible(true); @@ -2227,7 +2268,7 @@ `/api/telemetry?device_id=${encodeURIComponent(loadedSingleTrack.device_id)}&since=${range.min}&until=${range.max}&limit=500`, { cache: 'no-store' } ); - if (res.ok) telemetrySingle = await res.json(); + if (res.ok) telemetrySingle = normalizeTelemetry(await res.json()); } updateTimelineAtSingle(timelineCursor()); 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(loadedRxTrack.device_id)}&since=${range.min}&until=${range.max}&limit=500`, { cache: 'no-store' }) ]); - if (telTx.ok) telemetryTx = await telTx.json(); - if (telRx.ok) telemetryRx = await telRx.json(); + if (telTx.ok) telemetryTx = normalizeTelemetry(await telTx.json()); + if (telRx.ok) telemetryRx = normalizeTelemetry(await telRx.json()); } updateTimelineAt(timelineCursor()); } @@ -2326,7 +2367,7 @@ `/api/telemetry?device_id=${encodeURIComponent(loadedSingleTrack.device_id)}&since=${range.min}&until=${range.max}&limit=500`, { cache: 'no-store' } ); - if (telRes.ok) telemetrySingle = await telRes.json(); + if (telRes.ok) telemetrySingle = normalizeTelemetry(await telRes.json()); updateTimelineAtSingle(timelineCursor()); } 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(loadedRxTrack.device_id)}&since=${range.min}&until=${range.max}&limit=500`, { cache: 'no-store' }) ]); - if (telTx.ok) telemetryTx = await telTx.json(); - if (telRx.ok) telemetryRx = await telRx.json(); + if (telTx.ok) telemetryTx = normalizeTelemetry(await telTx.json()); + if (telRx.ok) telemetryRx = normalizeTelemetry(await telRx.json()); updateTimelineAt(timelineCursor()); } diff --git a/server/static/radio-ui.js b/server/static/radio-ui.js index b6bef6b..3bdeea6 100644 --- a/server/static/radio-ui.js +++ b/server/static/radio-ui.js @@ -116,8 +116,10 @@ function renderCompareGrid(txSnap, rxSnap, txId, rxId, changedTx, changedRx, staticOpen) { let html = '