Wideband autonomous SDR analysis engine forked from sdr-visual-suite
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

138 рядки
6.6KB

  1. (function (global) {
  2. function fmtHz(value) {
  3. if (!Number.isFinite(value)) return 'n/a';
  4. if (value >= 1e6) return `${(value / 1e6).toFixed(3)} MHz`;
  5. if (value >= 1e3) return `${(value / 1e3).toFixed(2)} kHz`;
  6. return `${Math.round(value)} Hz`;
  7. }
  8. function fmtAgeShort(ms) {
  9. if (!Number.isFinite(ms) || ms < 0) return '-';
  10. if (ms < 1000) return `${Math.round(ms)} ms`;
  11. if (ms < 60000) return `${(ms / 1000).toFixed(1)} s`;
  12. return `${Math.round(ms / 60000)} min`;
  13. }
  14. function applyStatusClass(el, level) {
  15. if (!el) return;
  16. el.classList.remove('status-val--ok', 'status-val--warn', 'status-val--bad');
  17. if (level === 'ok') el.classList.add('status-val--ok');
  18. if (level === 'warn') el.classList.add('status-val--warn');
  19. if (level === 'bad') el.classList.add('status-val--bad');
  20. }
  21. function levelSummary(level) {
  22. if (!level || typeof level !== 'object') return 'n/a';
  23. const bits = [];
  24. if (level.name) bits.push(level.name);
  25. if (Number.isFinite(level.fft_size) && level.fft_size > 0) bits.push(`${level.fft_size} bins`);
  26. if (Number.isFinite(level.span_hz) && level.span_hz > 0) bits.push(fmtHz(level.span_hz));
  27. return bits.join(' · ') || 'n/a';
  28. }
  29. function summarizeReason(reason) {
  30. if (!reason) return 'n/a';
  31. const parts = String(reason).split(':').filter(Boolean);
  32. if (!parts.length) return 'n/a';
  33. const tail = parts.slice(-3);
  34. const compact = tail.join(' › ');
  35. return compact.length > 64 ? compact.slice(0, 61) + '…' : compact;
  36. }
  37. function renderRefinementDetails(root, refinementInfo) {
  38. if (!root) return;
  39. const plan = refinementInfo?.plan || {};
  40. const queue = refinementInfo?.arbitration?.queue || {};
  41. const summary = refinementInfo?.arbitration?.decision_summary || {};
  42. const reasons = summary?.reasons || {};
  43. const topReason = Object.entries(reasons).sort((a, b) => Number(b[1]) - Number(a[1]))[0];
  44. const primary = refinementInfo?.surveillance_level_set?.primary || refinementInfo?.surveillance_level;
  45. const display = refinementInfo?.surveillance_level_set?.presentation || refinementInfo?.display_level;
  46. const spans = refinementInfo?.window_summary?.refinement || refinementInfo?.window_stats || {};
  47. const rows = [
  48. `Budget: ${(plan.selected || []).length}/${plan.budget || 0}`,
  49. `Queue: rec ${queue.record_queued || 0} · dec ${queue.decode_queued || 0}`,
  50. `Drop: snr ${plan.dropped_by_snr || 0} · budget ${plan.dropped_by_budget || 0}`,
  51. `Reason: ${topReason ? `${summarizeReason(topReason[0])} (${topReason[1]})` : 'n/a'}`,
  52. `Primary: ${levelSummary(primary)}`,
  53. `Display: ${levelSummary(display)}`,
  54. `Windows: ${spans.count ? `${spans.count} · ${fmtHz(spans.min_span_hz || 0)}-${fmtHz(spans.max_span_hz || 0)}` : 'n/a'}`
  55. ];
  56. root.innerHTML = rows.map((row) => `<div class="ops-line">${row}</div>`).join('');
  57. }
  58. function renderTelemetryEvents(root, telemetryLive) {
  59. if (!root) return;
  60. const items = Array.isArray(telemetryLive?.recent_events) ? telemetryLive.recent_events.slice(0, 6) : [];
  61. if (!items.length) {
  62. root.innerHTML = '<div class="ops-line ops-line--muted">No recent telemetry events.</div>';
  63. return;
  64. }
  65. root.innerHTML = items.map((item) => {
  66. const ts = item?.timestamp ? new Date(item.timestamp).toLocaleTimeString() : '--:--:--';
  67. const level = item?.level || 'info';
  68. const name = item?.name || item?.metric || item?.category || 'event';
  69. const detail = item?.message || item?.detail || item?.summary || '';
  70. const shortDetail = detail ? String(detail).slice(0, 72) : '';
  71. return `<div class="ops-line"><span class="ops-level ops-level--${level}">${level}</span><span class="ops-ts">${ts}</span><span class="ops-name">${name}${shortDetail ? ` · ${shortDetail}` : ''}</span></div>`;
  72. }).join('');
  73. }
  74. function create(elements) {
  75. function updateStatus(data) {
  76. const {
  77. wsState, wsLastMessageTs, apiState, configStatusText, refinementInfo, telemetryLive, sourceAgeMs
  78. } = data;
  79. if (elements.healthWs) {
  80. const age = wsLastMessageTs > 0 ? fmtAgeShort(Date.now() - wsLastMessageTs) : '-';
  81. elements.healthWs.textContent = `${wsState} · last ${age}`;
  82. applyStatusClass(elements.healthWs, wsState === 'live' ? 'ok' : (wsState === 'retrying' ? 'bad' : 'warn'));
  83. }
  84. if (elements.healthApi) {
  85. const latency = Number.isFinite(apiState?.latencyMs) ? `${apiState.latencyMs} ms` : 'n/a';
  86. const isOk = !!apiState?.ok;
  87. elements.healthApi.textContent = isOk ? `ok · ${latency}` : `degraded · ${apiState?.lastError || 'n/a'}`;
  88. applyStatusClass(elements.healthApi, isOk ? 'ok' : 'bad');
  89. }
  90. if (elements.healthConfig) {
  91. elements.healthConfig.textContent = configStatusText || '-';
  92. applyStatusClass(elements.healthConfig, /failed|offline/i.test(configStatusText || '') ? 'bad' : 'ok');
  93. }
  94. if (elements.healthRefine) {
  95. const plan = refinementInfo?.plan || {};
  96. const queue = refinementInfo?.arbitration?.queue || {};
  97. const budget = Number(plan?.budget || 0);
  98. const selected = Number((plan?.selected || []).length || 0);
  99. elements.healthRefine.textContent = `${selected}/${budget} · q ${queue.record_queued || 0}/${queue.decode_queued || 0}`;
  100. applyStatusClass(elements.healthRefine, budget > 0 && selected >= budget ? 'warn' : 'ok');
  101. }
  102. if (elements.healthTelemetry) {
  103. if (!telemetryLive) {
  104. elements.healthTelemetry.textContent = 'unavailable';
  105. applyStatusClass(elements.healthTelemetry, 'bad');
  106. } else {
  107. const enabled = telemetryLive.enabled === false ? 'off' : 'on';
  108. const collector = telemetryLive.collector || {};
  109. const recent = Array.isArray(telemetryLive.recent_events) ? telemetryLive.recent_events.length : 0;
  110. const heavy = collector.heavy_enabled ? 'heavy' : 'light';
  111. elements.healthTelemetry.textContent = `${enabled} · ${heavy} · events ${recent}`;
  112. applyStatusClass(elements.healthTelemetry, enabled === 'on' ? 'ok' : 'warn');
  113. }
  114. }
  115. if (elements.healthSource) {
  116. const text = Number.isFinite(sourceAgeMs) && sourceAgeMs >= 0 ? `${sourceAgeMs} ms` : 'n/a';
  117. elements.healthSource.textContent = text;
  118. applyStatusClass(elements.healthSource, Number.isFinite(sourceAgeMs) && sourceAgeMs < 1500 ? 'ok' : 'warn');
  119. }
  120. renderRefinementDetails(elements.refineDetails, refinementInfo);
  121. renderTelemetryEvents(elements.telemetryEvents, telemetryLive);
  122. }
  123. return { updateStatus };
  124. }
  125. global.OperatorPanel = { create };
  126. })(window);