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

499 рядки
30KB

  1. <!doctype html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8" />
  5. <meta name="viewport" content="width=device-width, initial-scale=1" />
  6. <title>Spectre UI</title>
  7. <link rel="preconnect" href="https://fonts.googleapis.com" />
  8. <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
  9. <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=Outfit:wght@300;400;500;600;700;800&display=swap" rel="stylesheet" />
  10. <link rel="stylesheet" href="style.css" />
  11. </head>
  12. <body>
  13. <div class="shell">
  14. <!-- ═══════════ TOPBAR ═══════════ -->
  15. <header class="topbar">
  16. <div class="brand">
  17. <svg class="brand-mark" width="28" height="28" viewBox="0 0 28 28">
  18. <circle cx="14" cy="14" r="11" fill="none" stroke="url(#bgrad)" stroke-width="2.2"/>
  19. <path d="M8 14 Q10.5 6, 14 14 Q17.5 22, 20 14" stroke="url(#bgrad)" stroke-width="1.8" fill="none"/>
  20. <defs><linearGradient id="bgrad" x1="0" y1="0" x2="28" y2="28"><stop offset="0%" stop-color="#00ffc8"/><stop offset="100%" stop-color="#0090ff"/></linearGradient></defs>
  21. </svg>
  22. <div class="brand-text">
  23. <div class="brand-kicker">SDR Ops Console</div>
  24. <div class="brand-title">Spectre<span class="brand-thin">UI</span></div>
  25. </div>
  26. </div>
  27. <div class="topbar-center"></div>
  28. <div class="topbar-right">
  29. <div class="ws-badge" id="wsBadge">Connecting</div>
  30. <div class="top-meta" id="metaLine">Waiting for signal frames…</div>
  31. <button class="shortcut-btn" id="shortcutToggle" type="button" title="Keyboard shortcuts">
  32. <svg width="14" height="14" viewBox="0 0 14 14"><rect x="1" y="4" width="12" height="8" rx="1.5" fill="none" stroke="currentColor" stroke-width="1.2"/><path d="M4 7h1M6.5 7h1M9 7h1M3.5 9.5h7" stroke="currentColor" stroke-width="0.8" stroke-linecap="round"/></svg>
  33. </button>
  34. </div>
  35. </header>
  36. <!-- ═══════════ MAIN LAYOUT ═══════════ -->
  37. <main class="layout">
  38. <!-- ──── STAGE (left) ──── -->
  39. <section class="stage panel">
  40. <div class="stage-head">
  41. <div>
  42. <div class="panel-title">RF Panorama</div>
  43. <div class="panel-sub" id="heroSubtitle">Live spectrum, waterfall and signal overlays</div>
  44. </div>
  45. <div class="stage-actions">
  46. <button class="act-btn" id="followBtn" type="button">Follow</button>
  47. <button class="act-btn" id="fitBtn" type="button">Fit</button>
  48. <button class="act-btn" id="resetMaxBtn" type="button">Reset Max</button>
  49. <label class="pill-toggle pill-toggle--compact"><input id="debugOverlayToggle" type="checkbox" checked /><span class="pt"><span class="pk"></span></span><span class="pl">CFAR/Debug</span></label>
  50. </div>
  51. </div>
  52. <!-- Hero Metrics -->
  53. <div class="hero-metrics" id="heroMetrics">
  54. <div class="metric"><span class="metric-lbl">Center</span><span class="metric-val" id="metricCenter">-</span></div>
  55. <div class="metric"><span class="metric-lbl">Span</span><span class="metric-val" id="metricSpan">-</span></div>
  56. <div class="metric"><span class="metric-lbl">Res</span><span class="metric-val" id="metricRes">-</span></div>
  57. <div class="metric"><span class="metric-lbl">Signals</span><span class="metric-val metric-val--accent" id="metricSignals">0</span></div>
  58. <div class="metric"><span class="metric-lbl">GPU</span><span class="metric-val" id="metricGpu">-</span></div>
  59. <div class="metric"><span class="metric-lbl">Source</span><span class="metric-val" id="metricSource">-</span></div>
  60. </div>
  61. <!-- Band Navigator -->
  62. <div class="nav-strip">
  63. <span class="nav-label">Band navigator</span>
  64. <canvas id="navCanvas"></canvas>
  65. </div>
  66. <!-- Spectrum + Waterfall Stack -->
  67. <div class="viz-stack">
  68. <div class="viz-card">
  69. <div class="viz-head">
  70. <span class="viz-label">Spectrum</span>
  71. <span class="viz-hint" id="spectrumMeta">Wheel = zoom · Drag = pan · Double-click = reset</span>
  72. <span class="viz-legend" id="cfarLegend">
  73. <span class="legend-swatch"></span>
  74. Adaptive CFAR threshold
  75. </span>
  76. </div>
  77. <canvas id="spectrum"></canvas>
  78. <div class="signal-popover" id="signalPopover" aria-hidden="true"></div>
  79. </div>
  80. <div class="viz-card">
  81. <div class="viz-head"><span class="viz-label">Waterfall</span><span class="viz-hint" id="waterfallMeta">Newest row at top</span></div>
  82. <canvas id="waterfall"></canvas>
  83. </div>
  84. </div>
  85. </section>
  86. <!-- ──── OPERATOR RAIL (right) ──── -->
  87. <aside class="rail panel">
  88. <div class="rail-head">
  89. <div>
  90. <div class="panel-title">Operator Rail</div>
  91. <div class="panel-sub" id="configStatus">Loading config…</div>
  92. </div>
  93. </div>
  94. <div class="rail-tabs" role="tablist" aria-label="Control tabs">
  95. <button class="rail-tab active" data-tab="radio" type="button">Radio</button>
  96. <button class="rail-tab" data-tab="detect" type="button">Detect</button>
  97. <button class="rail-tab" data-tab="record" type="button">Record</button>
  98. <button class="rail-tab" data-tab="refine" type="button">Refine</button>
  99. <button class="rail-tab" data-tab="policy" type="button">Policy</button>
  100. <button class="rail-tab" data-tab="signals" type="button">Signals</button>
  101. <button class="rail-tab" data-tab="events" type="button">Events</button>
  102. <button class="rail-tab" data-tab="health" type="button">Health</button>
  103. <button class="rail-tab" data-tab="recordings" type="button">Recordings</button>
  104. </div>
  105. <div class="rail-body">
  106. <!-- ── Radio Tab ── -->
  107. <section class="tab-panel active" data-panel="radio">
  108. <div class="form-group">
  109. <div class="grp-title">Tuning</div>
  110. <label class="field"><span>Center (MHz)</span><input id="centerInput" type="number" step="0.001" min="0" /></label>
  111. <div class="preset-row">
  112. <button class="preset-btn" data-center="7.1" type="button"><b>40m</b><small>7.1</small></button>
  113. <button class="preset-btn" data-center="14.1" type="button"><b>20m</b><small>14.1</small></button>
  114. <button class="preset-btn" data-center="18.1" type="button"><b>17m</b><small>18.1</small></button>
  115. <button class="preset-btn" data-center="145.5" type="button"><b>2m</b><small>145.5</small></button>
  116. </div>
  117. <label class="field"><span>Span (MHz)</span><input id="spanInput" type="number" step="0.05" min="0.01" /></label>
  118. <div class="field-pair">
  119. <label class="field"><span>Sample Rate</span>
  120. <select id="sampleRateSelect">
  121. <option value="0.5">0.5 MHz</option><option value="1.0">1.0 MHz</option>
  122. <option value="1.536">1.536 MHz</option><option value="2.048">2.048 MHz</option>
  123. <option value="2.5">2.5 MHz</option><option value="3.072">3.072 MHz</option>
  124. <option value="4.096">4.096 MHz</option>
  125. </select>
  126. </label>
  127. <label class="field"><span>Tuner BW</span>
  128. <select id="bwSelect">
  129. <option value="200">200 kHz</option><option value="300">300 kHz</option>
  130. <option value="600">600 kHz</option><option value="1536">1536 kHz</option>
  131. <option value="5000">5 MHz</option><option value="6000">6 MHz</option>
  132. <option value="7000">7 MHz</option><option value="8000">8 MHz</option>
  133. </select>
  134. </label>
  135. </div>
  136. </div>
  137. <div class="form-group">
  138. <div class="grp-title">DSP & Display</div>
  139. <label class="field"><span>FFT Size</span>
  140. <select id="fftSelect">
  141. <option value="512">512</option><option value="1024">1024</option>
  142. <option value="2048">2048</option><option value="4096">4096</option>
  143. <option value="8192">8192</option><option value="16384">16384</option>
  144. <option value="32768">32768</option>
  145. <option value="65536">65536</option>
  146. </select>
  147. </label>
  148. <div class="slider-field">
  149. <span>Gain</span>
  150. <div class="slider-row"><input id="gainRange" type="range" min="0" max="60" step="1" /><input id="gainInput" type="number" min="0" max="60" step="1" class="slider-num" /><em>dB</em></div>
  151. </div>
  152. <label class="field"><span>Averaging</span>
  153. <select id="avgSelect">
  154. <option value="0">Off</option><option value="0.4">Fast</option>
  155. <option value="0.2">Medium</option>
  156. <option value="0.1">Slow</option>
  157. </select>
  158. </label>
  159. <div class="toggle-grid">
  160. <label class="pill-toggle"><input id="agcToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">AGC</span></label>
  161. <label class="pill-toggle"><input id="dcToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">DC Block</span></label>
  162. <label class="pill-toggle"><input id="iqToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">IQ Bal</span></label>
  163. <label class="pill-toggle"><input id="maxHoldToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">Max Hold</span></label>
  164. <label class="pill-toggle"><input id="gpuToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">GPU FFT</span></label>
  165. </div>
  166. </div>
  167. </section>
  168. <!-- ── Detect Tab ── -->
  169. <section class="tab-panel" data-panel="detect">
  170. <div class="config-grid">
  171. <div class="form-group">
  172. <div class="grp-title">Classifier</div>
  173. <div class="form-hint">Signal classification behavior and switching thresholds.</div>
  174. <label class="field"><span>Classifier</span>
  175. <select id="classifierModeSelect">
  176. <option value="rule">Rule-Based</option>
  177. <option value="math">Mathematical</option>
  178. <option value="combined">Combined</option>
  179. </select>
  180. </label>
  181. </div>
  182. <div class="form-group">
  183. <div class="grp-title">Detector</div>
  184. <div class="form-hint">Thresholding, CFAR, and temporal stability for live carrier detection.</div>
  185. <div class="slider-field">
  186. <span>Threshold</span>
  187. <div class="slider-row"><input id="thresholdRange" type="range" min="-120" max="0" step="1" class="range--warn" /><input id="thresholdInput" type="number" min="-120" max="0" step="1" class="slider-num" /><em>dB</em></div>
  188. </div>
  189. <label class="field"><span>CFAR Guard (Hz)</span><input id="cfarGuardHzInput" type="number" step="100" min="0" /></label>
  190. <label class="field"><span>CFAR Train (Hz)</span><input id="cfarTrainHzInput" type="number" step="500" min="100" /></label>
  191. <label class="field"><span>CFAR Rank</span><input id="cfarRankInput" type="number" step="1" min="1" /></label>
  192. <label class="field"><span>CFAR Scale (dB)</span><input id="cfarScaleInput" type="number" step="0.5" min="0" /></label>
  193. <label class="field"><span>Min Duration (ms)</span><input id="minDurationInput" type="number" step="50" min="50" /></label>
  194. <label class="field"><span>Hold (ms)</span><input id="holdInput" type="number" step="50" min="50" /></label>
  195. <label class="field"><span>EMA Alpha</span><input id="emaAlphaInput" type="number" step="0.05" min="0" max="1" /></label>
  196. <label class="field"><span>Hysteresis (dB)</span><input id="hysteresisInput" type="number" step="1" min="0" /></label>
  197. <label class="field"><span>Min Stable Frames</span><input id="stableFramesInput" type="number" step="1" min="1" /></label>
  198. <label class="field"><span>Gap Tolerance (ms)</span><input id="gapToleranceInput" type="number" step="50" min="0" /></label>
  199. <label class="field"><span>Edge Margin (dB)</span><input id="edgeMarginInput" type="number" step="0.5" min="0" /></label>
  200. <label class="field"><span>Merge Gap (Hz)</span><input id="mergeGapInput" type="number" step="500" min="0" /></label>
  201. <label class="field"><span>Class History</span><input id="classHistoryInput" type="number" step="1" min="1" max="30" /></label>
  202. <label class="field"><span>Class Switch Ratio</span><input id="classSwitchInput" type="number" step="0.05" min="0.1" max="1.0" /></label>
  203. <label class="field"><span>CFAR Mode</span>
  204. <select id="cfarModeSelect">
  205. <option value="OFF">Off</option>
  206. <option value="CA">CA (Cell-Averaging)</option>
  207. <option value="OS">OS (Ordered-Statistic)</option>
  208. <option value="GOSCA">GOSCA (Greatest-Of)</option>
  209. <option value="CASO">CASO (Smallest-Of)</option>
  210. </select>
  211. </label>
  212. <div class="toggle-grid">
  213. <label class="pill-toggle"><input id="cfarWrapToggle" type="checkbox" checked /><span class="pt"><span class="pk"></span></span><span class="pl">Wrap-Around</span></label>
  214. </div>
  215. </div>
  216. </div>
  217. </section>
  218. <!-- ── Record Tab ── -->
  219. <section class="tab-panel" data-panel="record">
  220. <div class="config-grid">
  221. <div class="form-group">
  222. <div class="grp-title">Recorder</div>
  223. <div class="form-hint">Recording enablement, media types, decoding, and retention limits.</div>
  224. <div class="toggle-grid">
  225. <label class="pill-toggle"><input id="recEnableToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">Enabled</span></label>
  226. <label class="pill-toggle"><input id="recIQToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">Record IQ</span></label>
  227. <label class="pill-toggle"><input id="recAudioToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">Record Audio</span></label>
  228. <label class="pill-toggle"><input id="recDemodToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">Auto Demod</span></label>
  229. <label class="pill-toggle"><input id="recDecodeToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">Auto Decode</span></label>
  230. </div>
  231. <label class="field"><span>Min SNR (dB)</span><input id="recMinSNR" type="number" step="1" min="0" /></label>
  232. <label class="field"><span>Max Disk (MB)</span><input id="recMaxDisk" type="number" step="256" min="0" /></label>
  233. <label class="field"><span>Class Filter (CSV)</span><input id="recClassFilter" type="text" placeholder="e.g. NFM,USB" /></label>
  234. </div>
  235. </div>
  236. </section>
  237. <!-- ── Refine Tab ── -->
  238. <section class="tab-panel" data-panel="refine">
  239. <div class="config-grid">
  240. <div class="form-group">
  241. <div class="grp-title">Resources</div>
  242. <div class="form-hint">Concurrency and job-budget controls for refinement, recording, and decode.</div>
  243. <div class="field-pair">
  244. <label class="field"><span>Max Refinement</span><input id="resMaxRefine" type="number" step="1" min="0" /></label>
  245. <label class="field"><span>Max Record</span><input id="resMaxRecord" type="number" step="1" min="0" /></label>
  246. <label class="field"><span>Max Decode</span><input id="resMaxDecode" type="number" step="1" min="0" /></label>
  247. <label class="field"><span>Decision Hold (ms)</span><input id="resDecisionHold" type="number" step="250" min="0" /></label>
  248. </div>
  249. <div class="panel-sub">Budgets applied to refinement selection, recording, and decode decisions.</div>
  250. </div>
  251. <div class="form-group">
  252. <div class="grp-title">Refinement Windows</div>
  253. <div class="form-hint">Window sizing for refinement passes when candidate bandwidth is known or inferred.</div>
  254. <label class="field"><span>Auto Span</span>
  255. <select id="refineAutoSpan" class="ctrl-select">
  256. <option value="true">Auto</option>
  257. <option value="false">Manual</option>
  258. </select>
  259. </label>
  260. <div class="field-pair">
  261. <label class="field"><span>Min Span (Hz)</span><input id="refineMinSpan" type="number" step="500" min="0" /></label>
  262. <label class="field"><span>Max Span (Hz)</span><input id="refineMaxSpan" type="number" step="500" min="0" /></label>
  263. </div>
  264. <div class="panel-sub">Auto span uses modulation hints when candidate BW is missing. Min/Max clamp the final window.</div>
  265. </div>
  266. </div>
  267. </section>
  268. <!-- ── Policy Tab ── -->
  269. <section class="tab-panel" data-panel="policy">
  270. <div class="form-group">
  271. <div class="grp-title">Pipeline Policy</div>
  272. <div class="ops-list" id="policySummaryList"><div class="ops-line ops-line--muted">Loading policy…</div></div>
  273. </div>
  274. <div class="form-group">
  275. <div class="grp-title">Recommendations</div>
  276. <div class="ops-list" id="policyRecommendationList"><div class="ops-line ops-line--muted">Loading recommendations…</div></div>
  277. </div>
  278. </section>
  279. <!-- ── Signals Tab ── -->
  280. <section class="tab-panel" data-panel="signals">
  281. <div class="list-head"><span class="grp-title">Detected carriers</span><span class="count-pill" id="signalCountBadge">0 live</span></div>
  282. <div class="signal-list" id="signalList"><div class="empty-state">No live signals yet.</div></div>
  283. <div class="panel-sub" id="signalSummaryLine">Visible: 0 · Strongest: - · Selected: -</div>
  284. <div class="panel-sub" id="signalDecisionSummary">Decision: -</div>
  285. <div class="panel-sub" id="signalQueueSummary">Queue: -</div>
  286. <div class="field-pair">
  287. <label class="field"><span>Listen sec</span><input id="listenSeconds" type="number" min="1" max="10" step="1" value="2" /></label>
  288. <label class="field"><span>Listen mode</span>
  289. <select id="listenMode" class="ctrl-select">
  290. <option value="">Auto</option>
  291. <option value="AM">AM</option>
  292. <option value="NFM">NFM</option>
  293. <option value="WFM">WFM</option>
  294. <option value="WFM_STEREO">WFM_STEREO</option>
  295. <option value="USB">USB</option>
  296. <option value="LSB">LSB</option>
  297. <option value="CW">CW</option>
  298. </select>
  299. </label>
  300. </div>
  301. <button class="act-btn" id="liveListenBtn" type="button">Live Listen</button>
  302. <div class="listen-meta" id="listenMeta">
  303. <div class="listen-meta__head">
  304. <div class="listen-meta__title">Listen session</div>
  305. <div class="listen-meta__badge" id="listenMetaStatus">Idle</div>
  306. </div>
  307. <div class="listen-meta__primary">
  308. <span class="listen-meta__mode" id="listenMetaPlayback">-</span>
  309. <span class="listen-meta__stereo" id="listenMetaStereo">-</span>
  310. </div>
  311. <div class="listen-meta__secondary">
  312. <span id="listenMetaDemod">Demod -</span>
  313. <span id="listenMetaAudio">Audio -</span>
  314. </div>
  315. </div>
  316. </section>
  317. <!-- ── Events Tab ── -->
  318. <section class="tab-panel" data-panel="events">
  319. <div class="list-head"><span class="grp-title">Recent events</span><span class="count-pill" id="eventCountBadge">0 stored</span></div>
  320. <div class="event-list" id="eventList"><div class="empty-state">No events yet.</div></div>
  321. </section>
  322. <!-- ── Health Tab ── -->
  323. <section class="tab-panel" data-panel="health">
  324. <div class="health-grid">
  325. <div class="health-card"><span class="health-lbl">Buffer</span><span class="health-val" id="healthBuffer">-</span></div>
  326. <div class="health-card"><span class="health-lbl">Dropped</span><span class="health-val" id="healthDropped">-</span></div>
  327. <div class="health-card"><span class="health-lbl">Resets</span><span class="health-val" id="healthResets">-</span></div>
  328. <div class="health-card"><span class="health-lbl">Age</span><span class="health-val" id="healthAge">-</span></div>
  329. <div class="health-card"><span class="health-lbl">Refine plan</span><span class="health-val" id="healthRefinePlan">-</span></div>
  330. <div class="health-card"><span class="health-lbl">Refine windows</span><span class="health-val" id="healthRefineWindows">-</span></div>
  331. <div class="health-card health-card--wide"><span class="health-lbl">GPU status</span><span class="health-val" id="healthGpu">-</span></div>
  332. <div class="health-card health-card--wide"><span class="health-lbl">Render rate</span><span class="health-val" id="healthFps">-</span></div>
  333. </div>
  334. <div class="health-status-card">
  335. <div class="health-lbl">Operator Status</div>
  336. <div class="status-grid">
  337. <div class="status-row"><span class="status-key">WS</span><span class="status-val" id="healthWs">-</span></div>
  338. <div class="status-row"><span class="status-key">API</span><span class="status-val" id="healthApi">-</span></div>
  339. <div class="status-row"><span class="status-key">Config</span><span class="status-val" id="healthConfig">-</span></div>
  340. <div class="status-row"><span class="status-key">Source</span><span class="status-val" id="healthSource">-</span></div>
  341. <div class="status-row"><span class="status-key">Refinement</span><span class="status-val" id="healthRefine">-</span></div>
  342. <div class="status-row"><span class="status-key">Telemetry</span><span class="status-val" id="healthTelemetry">-</span></div>
  343. </div>
  344. </div>
  345. <div class="health-status-card">
  346. <div class="health-lbl">Refinement Snapshot</div>
  347. <div class="ops-list" id="refineDetails"><div class="ops-line ops-line--muted">Loading refinement snapshot...</div></div>
  348. </div>
  349. <div class="health-status-card">
  350. <div class="health-lbl">Telemetry Recent Events</div>
  351. <div class="ops-list" id="telemetryEventList"><div class="ops-line ops-line--muted">Loading telemetry events...</div></div>
  352. </div>
  353. </section>
  354. <section class="tab-panel" data-panel="recordings">
  355. <div class="list-head"><span class="grp-title">Recordings</span><span class="count-pill" id="recordingCountBadge">0</span></div>
  356. <div class="event-list" id="recordingList"><div class="empty-state">No recordings yet.</div></div>
  357. </section>
  358. </div>
  359. </aside>
  360. <!-- ──── TIMELINE (bottom) ──── -->
  361. <section class="tl-panel panel">
  362. <div class="tl-head">
  363. <div>
  364. <div class="panel-title">Activity Timeline</div>
  365. <div class="panel-sub" id="timelineRange">Waiting for events…</div>
  366. </div>
  367. <div class="tl-actions">
  368. <button class="act-btn" id="timelineFollowBtn" type="button">Follow</button>
  369. <button class="act-btn" id="timelineFreezeBtn" type="button">Freeze</button>
  370. </div>
  371. </div>
  372. <div class="tl-stack">
  373. <div class="viz-card viz-card--compact">
  374. <div class="viz-head"><span class="viz-label">Occupancy strip</span><span class="viz-hint">Band activity density</span></div>
  375. <canvas id="occupancy"></canvas>
  376. </div>
  377. <div class="viz-card">
  378. <div class="viz-head"><span class="viz-label">Event blocks</span><span class="viz-hint">Click block → inspector</span></div>
  379. <canvas id="timeline"></canvas>
  380. </div>
  381. </div>
  382. </section>
  383. </main>
  384. <!-- ═══════════ INSPECTOR DRAWER ═══════════ -->
  385. <aside class="inspector" id="eventDrawer" aria-hidden="true">
  386. <div class="insp-head">
  387. <div>
  388. <div class="panel-title">Signal Inspector</div>
  389. <div class="panel-sub" id="detailSubtitle">No event selected</div>
  390. </div>
  391. <div class="insp-actions">
  392. <button class="act-btn" id="jumpToEventBtn" type="button">Go to freq</button>
  393. <select id="decodeMode" class="ctrl-select">
  394. <option value="FT8">FT8</option>
  395. <option value="WSPR">WSPR</option>
  396. <option value="DMR">DMR</option>
  397. <option value="D-STAR">D-STAR</option>
  398. <option value="FSK">FSK</option>
  399. <option value="PSK">PSK</option>
  400. </select>
  401. <button class="act-btn" id="exportEventBtn" type="button">Export</button>
  402. <button class="act-btn" id="liveListenEventBtn" type="button">Listen</button>
  403. <button class="act-btn" id="decodeEventBtn" type="button">Decode</button>
  404. <button class="act-btn act-btn--danger" id="drawerClose" type="button">Close</button>
  405. </div>
  406. </div>
  407. <div class="insp-body">
  408. <div class="detail-grid">
  409. <div class="detail-item"><span class="detail-label">Center</span><span id="detailCenter">-</span></div>
  410. <div class="detail-item"><span class="detail-label">Bandwidth</span><span id="detailBw">-</span></div>
  411. <div class="detail-item"><span class="detail-label">Start</span><span id="detailStart">-</span></div>
  412. <div class="detail-item"><span class="detail-label">End</span><span id="detailEnd">-</span></div>
  413. <div class="detail-item"><span class="detail-label">SNR</span><span id="detailSnr">-</span></div>
  414. <div class="detail-item"><span class="detail-label">Duration</span><span id="detailDur">-</span></div>
  415. <div class="detail-item"><span class="detail-label">Class</span><span id="detailClass">-</span></div>
  416. </div>
  417. <div class="viz-card insp-viz">
  418. <div class="viz-head"><span class="viz-label">Local spectrum slice</span><span class="viz-hint">Around selected event</span></div>
  419. <canvas id="detailSpectrogram"></canvas>
  420. </div>
  421. <div class="insp-note">Ready for: IQ snippets, clip playback, bookmarks, annotations.</div>
  422. <div class="insp-note" id="recordingMeta">Recording: -</div>
  423. <div class="insp-note" id="decodeResult">Decode: -</div>
  424. <div class="insp-note" id="classifierScores">Classifier scores: -</div>
  425. <div class="score-bars" id="classifierScoreBars"></div>
  426. <div class="insp-note">
  427. <a id="recordingMetaLink" href="#" target="_blank">meta.json</a> ·
  428. <a id="recordingIQLink" href="#" target="_blank">IQ</a> ·
  429. <a id="recordingAudioLink" href="#" target="_blank">Audio</a>
  430. </div>
  431. </div>
  432. </aside>
  433. <!-- ═══════════ KEYBOARD OVERLAY ═══════════ -->
  434. <div class="kb-overlay" id="kbOverlay">
  435. <div class="kb-card">
  436. <div class="kb-title">Keyboard Shortcuts</div>
  437. <div class="kb-grid">
  438. <kbd>Space</kbd><span>Follow live</span>
  439. <kbd>F</kbd><span>Fit view</span>
  440. <kbd>M</kbd><span>Toggle max hold</span>
  441. <kbd>G</kbd><span>Toggle GPU FFT</span>
  442. <kbd>[ ]</kbd><span>Zoom in/out</span>
  443. <kbd>← →</kbd><span>Pan left/right</span>
  444. <kbd>?</kbd><span>This overlay</span>
  445. </div>
  446. <button class="act-btn kb-close" id="kbClose" type="button">Close</button>
  447. </div>
  448. </div>
  449. </div>
  450. <script src="api-client.js"></script>
  451. <script src="operator-panel.js"></script>
  452. <script src="app.js"></script>
  453. <script>
  454. // Keyboard overlay toggle (new feature)
  455. const kbOverlay = document.getElementById('kbOverlay');
  456. const kbClose = document.getElementById('kbClose');
  457. const kbToggle = document.getElementById('shortcutToggle');
  458. function toggleKb() { kbOverlay.classList.toggle('open'); }
  459. kbToggle.addEventListener('click', toggleKb);
  460. kbClose.addEventListener('click', toggleKb);
  461. window.addEventListener('keydown', (e) => {
  462. if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') return;
  463. if (e.key === '?') { e.preventDefault(); toggleKb(); }
  464. if (e.key === 'Escape' && kbOverlay.classList.contains('open')) toggleKb();
  465. });
  466. </script>
  467. </body>
  468. </html>