Wideband autonomous SDR analysis engine forked from sdr-visual-suite
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

610 linhas
23KB

  1. package main
  2. import (
  3. "context"
  4. "encoding/json"
  5. "errors"
  6. "io"
  7. "log"
  8. "net/http"
  9. "os"
  10. "path/filepath"
  11. "strconv"
  12. "strings"
  13. "sync"
  14. "time"
  15. "sdr-wideband-suite/internal/config"
  16. "sdr-wideband-suite/internal/detector"
  17. "sdr-wideband-suite/internal/events"
  18. fftutil "sdr-wideband-suite/internal/fft"
  19. "sdr-wideband-suite/internal/pipeline"
  20. "sdr-wideband-suite/internal/recorder"
  21. "sdr-wideband-suite/internal/runtime"
  22. "sdr-wideband-suite/internal/telemetry"
  23. )
  24. func registerAPIHandlers(mux *http.ServeMux, cfgPath string, cfgManager *runtime.Manager, srcMgr *sourceManager, dspUpdates chan dspUpdate, gpuState *gpuStatus, recMgr *recorder.Manager, sigSnap *signalSnapshot, eventMu *sync.RWMutex, phaseSnap *phaseSnapshot, telem *telemetry.Collector) {
  25. mux.HandleFunc("/api/config", func(w http.ResponseWriter, r *http.Request) {
  26. w.Header().Set("Content-Type", "application/json")
  27. switch r.Method {
  28. case http.MethodGet:
  29. _ = json.NewEncoder(w).Encode(cfgManager.Snapshot())
  30. case http.MethodPost:
  31. var update runtime.ConfigUpdate
  32. if err := json.NewDecoder(r.Body).Decode(&update); err != nil {
  33. http.Error(w, "invalid json", http.StatusBadRequest)
  34. return
  35. }
  36. prev := cfgManager.Snapshot()
  37. next, err := cfgManager.ApplyConfig(update)
  38. if err != nil {
  39. http.Error(w, err.Error(), http.StatusBadRequest)
  40. return
  41. }
  42. if update.Pipeline != nil && update.Pipeline.Profile != nil {
  43. if prof, ok := pipeline.ResolveProfile(next, *update.Pipeline.Profile); ok {
  44. pipeline.MergeProfile(&next, prof)
  45. cfgManager.Replace(next)
  46. }
  47. }
  48. sourceChanged := prev.CenterHz != next.CenterHz || prev.SampleRate != next.SampleRate || prev.GainDb != next.GainDb || prev.AGC != next.AGC || prev.TunerBwKHz != next.TunerBwKHz
  49. if sourceChanged {
  50. if err := srcMgr.ApplyConfig(next); err != nil {
  51. cfgManager.Replace(prev)
  52. http.Error(w, "failed to apply source config", http.StatusInternalServerError)
  53. return
  54. }
  55. }
  56. if err := config.Save(cfgPath, next); err != nil {
  57. log.Printf("config save failed: %v", err)
  58. }
  59. detChanged := prev.Detector.ThresholdDb != next.Detector.ThresholdDb ||
  60. prev.Detector.MinDurationMs != next.Detector.MinDurationMs ||
  61. prev.Detector.HoldMs != next.Detector.HoldMs ||
  62. prev.Detector.EmaAlpha != next.Detector.EmaAlpha ||
  63. prev.Detector.HysteresisDb != next.Detector.HysteresisDb ||
  64. prev.Detector.MinStableFrames != next.Detector.MinStableFrames ||
  65. prev.Detector.GapToleranceMs != next.Detector.GapToleranceMs ||
  66. prev.Detector.CFARMode != next.Detector.CFARMode ||
  67. prev.Detector.CFARGuardHz != next.Detector.CFARGuardHz ||
  68. prev.Detector.CFARTrainHz != next.Detector.CFARTrainHz ||
  69. prev.Detector.CFARRank != next.Detector.CFARRank ||
  70. prev.Detector.CFARScaleDb != next.Detector.CFARScaleDb ||
  71. prev.Detector.CFARWrapAround != next.Detector.CFARWrapAround ||
  72. prev.SampleRate != next.SampleRate ||
  73. prev.FFTSize != next.FFTSize
  74. windowChanged := prev.FFTSize != next.FFTSize
  75. var newDet *detector.Detector
  76. var newWindow []float64
  77. if detChanged {
  78. newDet = detector.New(next.Detector, next.SampleRate, next.FFTSize)
  79. }
  80. if windowChanged {
  81. newWindow = fftutil.Hann(next.FFTSize)
  82. }
  83. pushDSPUpdate(dspUpdates, dspUpdate{cfg: next, det: newDet, window: newWindow, dcBlock: next.DCBlock, iqBalance: next.IQBalance, useGPUFFT: next.UseGPUFFT})
  84. _ = json.NewEncoder(w).Encode(next)
  85. default:
  86. http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
  87. }
  88. })
  89. mux.HandleFunc("/api/sdr/settings", func(w http.ResponseWriter, r *http.Request) {
  90. w.Header().Set("Content-Type", "application/json")
  91. if r.Method != http.MethodPost {
  92. http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
  93. return
  94. }
  95. var update runtime.SettingsUpdate
  96. if err := json.NewDecoder(r.Body).Decode(&update); err != nil {
  97. http.Error(w, "invalid json", http.StatusBadRequest)
  98. return
  99. }
  100. prev := cfgManager.Snapshot()
  101. next, err := cfgManager.ApplySettings(update)
  102. if err != nil {
  103. http.Error(w, err.Error(), http.StatusBadRequest)
  104. return
  105. }
  106. if prev.AGC != next.AGC || prev.TunerBwKHz != next.TunerBwKHz {
  107. if err := srcMgr.ApplyConfig(next); err != nil {
  108. cfgManager.Replace(prev)
  109. http.Error(w, "failed to apply sdr settings", http.StatusInternalServerError)
  110. return
  111. }
  112. }
  113. if prev.DCBlock != next.DCBlock || prev.IQBalance != next.IQBalance {
  114. pushDSPUpdate(dspUpdates, dspUpdate{cfg: next, dcBlock: next.DCBlock, iqBalance: next.IQBalance})
  115. }
  116. if err := config.Save(cfgPath, next); err != nil {
  117. log.Printf("config save failed: %v", err)
  118. }
  119. _ = json.NewEncoder(w).Encode(next)
  120. })
  121. mux.HandleFunc("/api/stats", func(w http.ResponseWriter, r *http.Request) {
  122. w.Header().Set("Content-Type", "application/json")
  123. _ = json.NewEncoder(w).Encode(srcMgr.Stats())
  124. })
  125. mux.HandleFunc("/api/gpu", func(w http.ResponseWriter, r *http.Request) {
  126. w.Header().Set("Content-Type", "application/json")
  127. _ = json.NewEncoder(w).Encode(gpuState.snapshot())
  128. })
  129. mux.HandleFunc("/api/pipeline/policy", func(w http.ResponseWriter, r *http.Request) {
  130. w.Header().Set("Content-Type", "application/json")
  131. cfg := cfgManager.Snapshot()
  132. _ = json.NewEncoder(w).Encode(pipeline.PolicyFromConfig(cfg))
  133. })
  134. mux.HandleFunc("/api/pipeline/recommendations", func(w http.ResponseWriter, r *http.Request) {
  135. w.Header().Set("Content-Type", "application/json")
  136. cfg := cfgManager.Snapshot()
  137. policy := pipeline.PolicyFromConfig(cfg)
  138. budget := pipeline.BudgetModelFromPolicy(policy)
  139. recommend := map[string]any{
  140. "profile": policy.Profile,
  141. "mode": policy.Mode,
  142. "intent": policy.Intent,
  143. "surveillance_strategy": policy.SurveillanceStrategy,
  144. "surveillance_detection": policy.SurveillanceDetection,
  145. "refinement_strategy": policy.RefinementStrategy,
  146. "monitor_center_hz": policy.MonitorCenterHz,
  147. "monitor_start_hz": policy.MonitorStartHz,
  148. "monitor_end_hz": policy.MonitorEndHz,
  149. "monitor_span_hz": policy.MonitorSpanHz,
  150. "monitor_windows": policy.MonitorWindows,
  151. "signal_priorities": policy.SignalPriorities,
  152. "auto_record_classes": policy.AutoRecordClasses,
  153. "auto_decode_classes": policy.AutoDecodeClasses,
  154. "refinement_jobs": policy.MaxRefinementJobs,
  155. "refinement_detail_fft": policy.RefinementDetailFFTSize,
  156. "refinement_auto_span": policy.RefinementAutoSpan,
  157. "refinement_min_span_hz": policy.RefinementMinSpanHz,
  158. "refinement_max_span_hz": policy.RefinementMaxSpanHz,
  159. "budgets": budget,
  160. }
  161. _ = json.NewEncoder(w).Encode(recommend)
  162. })
  163. mux.HandleFunc("/api/refinement", func(w http.ResponseWriter, r *http.Request) {
  164. w.Header().Set("Content-Type", "application/json")
  165. snap := phaseSnap.Snapshot()
  166. windowSummary := buildWindowSummary(snap.refinement.Input.Plan, snap.refinement.Input.Windows, snap.surveillance.Candidates, snap.refinement.Input.WorkItems, snap.refinement.Result.Decisions)
  167. var windowStats *RefinementWindowStats
  168. var monitorSummary []pipeline.MonitorWindowStats
  169. if windowSummary != nil {
  170. windowStats = windowSummary.Refinement
  171. monitorSummary = windowSummary.MonitorWindows
  172. }
  173. if windowStats == nil {
  174. windowStats = buildWindowStats(snap.refinement.Input.Windows)
  175. }
  176. if len(monitorSummary) == 0 && len(snap.refinement.Input.Plan.MonitorWindowStats) > 0 {
  177. monitorSummary = snap.refinement.Input.Plan.MonitorWindowStats
  178. }
  179. arbitration := buildArbitrationSnapshot(snap.refinement, snap.arbitration)
  180. levelSet := snap.surveillance.LevelSet
  181. spectraBins := map[string]int{}
  182. for _, spec := range snap.surveillance.Spectra {
  183. if len(spec.Spectrum) == 0 {
  184. continue
  185. }
  186. spectraBins[spec.Level.Name] = len(spec.Spectrum)
  187. }
  188. levelSummaries := buildSurveillanceLevelSummaries(levelSet, snap.surveillance.Spectra)
  189. candidateSources := buildCandidateSourceSummary(snap.surveillance.Candidates)
  190. candidateEvidence := buildCandidateEvidenceSummary(snap.surveillance.Candidates)
  191. candidateEvidenceStates := buildCandidateEvidenceStateSummary(snap.surveillance.Candidates)
  192. candidateWindows := buildCandidateWindowSummary(snap.surveillance.Candidates, snap.refinement.Input.Plan.MonitorWindows)
  193. out := map[string]any{
  194. "plan": snap.refinement.Input.Plan,
  195. "windows": snap.refinement.Input.Windows,
  196. "window_stats": windowStats,
  197. "window_summary": windowSummary,
  198. "request": snap.refinement.Input.Request,
  199. "context": snap.refinement.Input.Context,
  200. "detail_level": snap.refinement.Input.Detail,
  201. "arbitration": arbitration,
  202. "work_items": snap.refinement.Input.WorkItems,
  203. "candidates": len(snap.refinement.Input.Candidates),
  204. "scheduled": len(snap.refinement.Input.Scheduled),
  205. "signals": len(snap.refinement.Result.Signals),
  206. "decisions": len(snap.refinement.Result.Decisions),
  207. "surveillance_level": snap.surveillance.Level,
  208. "surveillance_levels": snap.surveillance.Levels,
  209. "surveillance_level_set": levelSet,
  210. "surveillance_detection_policy": snap.surveillance.DetectionPolicy,
  211. "surveillance_detection_levels": levelSet.Detection,
  212. "surveillance_support_levels": levelSet.Support,
  213. "surveillance_active_levels": func() []pipeline.AnalysisLevel {
  214. if len(levelSet.All) > 0 {
  215. return levelSet.All
  216. }
  217. active := make([]pipeline.AnalysisLevel, 0, len(snap.surveillance.Levels)+1)
  218. if snap.surveillance.Level.Name != "" {
  219. active = append(active, snap.surveillance.Level)
  220. }
  221. active = append(active, snap.surveillance.Levels...)
  222. if snap.surveillance.DisplayLevel.Name != "" {
  223. active = append(active, snap.surveillance.DisplayLevel)
  224. }
  225. return active
  226. }(),
  227. "surveillance_level_summary": levelSummaries,
  228. "surveillance_spectra_bins": spectraBins,
  229. "candidate_sources": candidateSources,
  230. "candidate_evidence": candidateEvidence,
  231. "candidate_evidence_states": candidateEvidenceStates,
  232. "candidate_windows": candidateWindows,
  233. "monitor_windows": snap.refinement.Input.Plan.MonitorWindows,
  234. "monitor_window_stats": monitorSummary,
  235. "display_level": snap.surveillance.DisplayLevel,
  236. "refinement_level": snap.refinement.Input.Level,
  237. "presentation_level": snap.presentation,
  238. }
  239. _ = json.NewEncoder(w).Encode(out)
  240. })
  241. mux.HandleFunc("/api/events", func(w http.ResponseWriter, r *http.Request) {
  242. w.Header().Set("Content-Type", "application/json")
  243. limit := 200
  244. if v := r.URL.Query().Get("limit"); v != "" {
  245. if parsed, err := strconv.Atoi(v); err == nil {
  246. limit = parsed
  247. }
  248. }
  249. var since time.Time
  250. if v := r.URL.Query().Get("since"); v != "" {
  251. if parsed, err := parseSince(v); err == nil {
  252. since = parsed
  253. } else {
  254. http.Error(w, "invalid since", http.StatusBadRequest)
  255. return
  256. }
  257. }
  258. snap := cfgManager.Snapshot()
  259. eventMu.RLock()
  260. evs, err := events.ReadRecent(snap.EventPath, limit, since)
  261. eventMu.RUnlock()
  262. if err != nil {
  263. http.Error(w, "failed to read events", http.StatusInternalServerError)
  264. return
  265. }
  266. _ = json.NewEncoder(w).Encode(evs)
  267. })
  268. mux.HandleFunc("/api/signals", func(w http.ResponseWriter, r *http.Request) {
  269. w.Header().Set("Content-Type", "application/json")
  270. if sigSnap == nil {
  271. _ = json.NewEncoder(w).Encode([]detector.Signal{})
  272. return
  273. }
  274. _ = json.NewEncoder(w).Encode(sigSnap.get())
  275. })
  276. mux.HandleFunc("/api/candidates", func(w http.ResponseWriter, r *http.Request) {
  277. w.Header().Set("Content-Type", "application/json")
  278. if sigSnap == nil {
  279. _ = json.NewEncoder(w).Encode([]pipeline.Candidate{})
  280. return
  281. }
  282. sigs := sigSnap.get()
  283. _ = json.NewEncoder(w).Encode(pipeline.CandidatesFromSignals(sigs, "tracked-signal-snapshot"))
  284. })
  285. mux.HandleFunc("/api/decoders", func(w http.ResponseWriter, r *http.Request) {
  286. w.Header().Set("Content-Type", "application/json")
  287. _ = json.NewEncoder(w).Encode(decoderKeys(cfgManager.Snapshot()))
  288. })
  289. mux.HandleFunc("/api/recordings", func(w http.ResponseWriter, r *http.Request) {
  290. if r.Method != http.MethodGet {
  291. http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
  292. return
  293. }
  294. w.Header().Set("Content-Type", "application/json")
  295. snap := cfgManager.Snapshot()
  296. list, err := recorder.ListRecordings(snap.Recorder.OutputDir)
  297. if err != nil {
  298. http.Error(w, "failed to list recordings", http.StatusInternalServerError)
  299. return
  300. }
  301. _ = json.NewEncoder(w).Encode(list)
  302. })
  303. mux.HandleFunc("/api/recordings/", func(w http.ResponseWriter, r *http.Request) {
  304. w.Header().Set("Content-Type", "application/json")
  305. id := strings.TrimPrefix(r.URL.Path, "/api/recordings/")
  306. if id == "" {
  307. http.Error(w, "missing id", http.StatusBadRequest)
  308. return
  309. }
  310. snap := cfgManager.Snapshot()
  311. base := filepath.Clean(filepath.Join(snap.Recorder.OutputDir, id))
  312. if !strings.HasPrefix(base, filepath.Clean(snap.Recorder.OutputDir)) {
  313. http.Error(w, "invalid path", http.StatusBadRequest)
  314. return
  315. }
  316. if r.URL.Path == "/api/recordings/"+id+"/audio" {
  317. http.ServeFile(w, r, filepath.Join(base, "audio.wav"))
  318. return
  319. }
  320. if r.URL.Path == "/api/recordings/"+id+"/iq" {
  321. http.ServeFile(w, r, filepath.Join(base, "signal.cf32"))
  322. return
  323. }
  324. if r.URL.Path == "/api/recordings/"+id+"/decode" {
  325. mode := r.URL.Query().Get("mode")
  326. cmd := buildDecoderMap(cfgManager.Snapshot())[mode]
  327. if cmd == "" {
  328. http.Error(w, "decoder not configured", http.StatusBadRequest)
  329. return
  330. }
  331. meta, err := recorder.ReadMeta(filepath.Join(base, "meta.json"))
  332. if err != nil {
  333. http.Error(w, "meta read failed", http.StatusInternalServerError)
  334. return
  335. }
  336. audioPath := filepath.Join(base, "audio.wav")
  337. if _, errStat := os.Stat(audioPath); errStat != nil {
  338. audioPath = ""
  339. }
  340. res, err := recorder.DecodeOnDemand(cmd, filepath.Join(base, "signal.cf32"), meta.SampleRate, audioPath)
  341. if err != nil {
  342. http.Error(w, res.Stderr, http.StatusInternalServerError)
  343. return
  344. }
  345. _ = json.NewEncoder(w).Encode(res)
  346. return
  347. }
  348. http.ServeFile(w, r, filepath.Join(base, "meta.json"))
  349. })
  350. mux.HandleFunc("/api/streams", func(w http.ResponseWriter, r *http.Request) {
  351. w.Header().Set("Content-Type", "application/json")
  352. n := recMgr.ActiveStreams()
  353. _ = json.NewEncoder(w).Encode(map[string]any{"active_sessions": n})
  354. })
  355. mux.HandleFunc("/api/demod", func(w http.ResponseWriter, r *http.Request) {
  356. if r.Method != http.MethodGet {
  357. http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
  358. return
  359. }
  360. q := r.URL.Query()
  361. freq, _ := strconv.ParseFloat(q.Get("freq"), 64)
  362. bw, _ := strconv.ParseFloat(q.Get("bw"), 64)
  363. sec, _ := strconv.Atoi(q.Get("sec"))
  364. if sec < 1 {
  365. sec = 1
  366. }
  367. if sec > 10 {
  368. sec = 10
  369. }
  370. mode := q.Get("mode")
  371. data, _, err := recMgr.DemodLive(freq, bw, mode, sec)
  372. if err != nil {
  373. http.Error(w, err.Error(), http.StatusBadRequest)
  374. return
  375. }
  376. w.Header().Set("Content-Type", "audio/wav")
  377. _, _ = w.Write(data)
  378. })
  379. mux.HandleFunc("/api/debug/telemetry/live", func(w http.ResponseWriter, r *http.Request) {
  380. w.Header().Set("Content-Type", "application/json")
  381. if telem == nil {
  382. _ = json.NewEncoder(w).Encode(map[string]any{"enabled": false, "error": "telemetry unavailable"})
  383. return
  384. }
  385. _ = json.NewEncoder(w).Encode(telem.LiveSnapshot())
  386. })
  387. mux.HandleFunc("/api/debug/telemetry/history", func(w http.ResponseWriter, r *http.Request) {
  388. w.Header().Set("Content-Type", "application/json")
  389. if telem == nil {
  390. http.Error(w, "telemetry unavailable", http.StatusServiceUnavailable)
  391. return
  392. }
  393. query, err := telemetryQueryFromRequest(r)
  394. if err != nil {
  395. http.Error(w, err.Error(), http.StatusBadRequest)
  396. return
  397. }
  398. items, err := telem.QueryMetrics(query)
  399. if err != nil {
  400. http.Error(w, err.Error(), http.StatusInternalServerError)
  401. return
  402. }
  403. _ = json.NewEncoder(w).Encode(map[string]any{"items": items, "count": len(items)})
  404. })
  405. mux.HandleFunc("/api/debug/telemetry/events", func(w http.ResponseWriter, r *http.Request) {
  406. w.Header().Set("Content-Type", "application/json")
  407. if telem == nil {
  408. http.Error(w, "telemetry unavailable", http.StatusServiceUnavailable)
  409. return
  410. }
  411. query, err := telemetryQueryFromRequest(r)
  412. if err != nil {
  413. http.Error(w, err.Error(), http.StatusBadRequest)
  414. return
  415. }
  416. items, err := telem.QueryEvents(query)
  417. if err != nil {
  418. http.Error(w, err.Error(), http.StatusInternalServerError)
  419. return
  420. }
  421. _ = json.NewEncoder(w).Encode(map[string]any{"items": items, "count": len(items)})
  422. })
  423. mux.HandleFunc("/api/debug/telemetry/config", func(w http.ResponseWriter, r *http.Request) {
  424. w.Header().Set("Content-Type", "application/json")
  425. if telem == nil {
  426. http.Error(w, "telemetry unavailable", http.StatusServiceUnavailable)
  427. return
  428. }
  429. switch r.Method {
  430. case http.MethodGet:
  431. _ = json.NewEncoder(w).Encode(map[string]any{
  432. "collector": telem.Config(),
  433. "config": cfgManager.Snapshot().Debug.Telemetry,
  434. })
  435. case http.MethodPost:
  436. var update struct {
  437. Enabled *bool `json:"enabled"`
  438. HeavyEnabled *bool `json:"heavy_enabled"`
  439. HeavySampleEvery *int `json:"heavy_sample_every"`
  440. MetricSampleEvery *int `json:"metric_sample_every"`
  441. MetricHistoryMax *int `json:"metric_history_max"`
  442. EventHistoryMax *int `json:"event_history_max"`
  443. RetentionSeconds *int `json:"retention_seconds"`
  444. PersistEnabled *bool `json:"persist_enabled"`
  445. PersistDir *string `json:"persist_dir"`
  446. RotateMB *int `json:"rotate_mb"`
  447. KeepFiles *int `json:"keep_files"`
  448. }
  449. if err := json.NewDecoder(r.Body).Decode(&update); err != nil {
  450. http.Error(w, "invalid json", http.StatusBadRequest)
  451. return
  452. }
  453. next := cfgManager.Snapshot()
  454. cur := next.Debug.Telemetry
  455. if update.Enabled != nil {
  456. cur.Enabled = *update.Enabled
  457. }
  458. if update.HeavyEnabled != nil {
  459. cur.HeavyEnabled = *update.HeavyEnabled
  460. }
  461. if update.HeavySampleEvery != nil {
  462. cur.HeavySampleEvery = *update.HeavySampleEvery
  463. }
  464. if update.MetricSampleEvery != nil {
  465. cur.MetricSampleEvery = *update.MetricSampleEvery
  466. }
  467. if update.MetricHistoryMax != nil {
  468. cur.MetricHistoryMax = *update.MetricHistoryMax
  469. }
  470. if update.EventHistoryMax != nil {
  471. cur.EventHistoryMax = *update.EventHistoryMax
  472. }
  473. if update.RetentionSeconds != nil {
  474. cur.RetentionSeconds = *update.RetentionSeconds
  475. }
  476. if update.PersistEnabled != nil {
  477. cur.PersistEnabled = *update.PersistEnabled
  478. }
  479. if update.PersistDir != nil && *update.PersistDir != "" {
  480. cur.PersistDir = *update.PersistDir
  481. }
  482. if update.RotateMB != nil {
  483. cur.RotateMB = *update.RotateMB
  484. }
  485. if update.KeepFiles != nil {
  486. cur.KeepFiles = *update.KeepFiles
  487. }
  488. next.Debug.Telemetry = cur
  489. cfgManager.Replace(next)
  490. if err := config.Save(cfgPath, next); err != nil {
  491. log.Printf("telemetry config save failed: %v", err)
  492. }
  493. err := telem.Configure(telemetry.Config{
  494. Enabled: cur.Enabled,
  495. HeavyEnabled: cur.HeavyEnabled,
  496. HeavySampleEvery: cur.HeavySampleEvery,
  497. MetricSampleEvery: cur.MetricSampleEvery,
  498. MetricHistoryMax: cur.MetricHistoryMax,
  499. EventHistoryMax: cur.EventHistoryMax,
  500. Retention: time.Duration(cur.RetentionSeconds) * time.Second,
  501. PersistEnabled: cur.PersistEnabled,
  502. PersistDir: cur.PersistDir,
  503. RotateMB: cur.RotateMB,
  504. KeepFiles: cur.KeepFiles,
  505. })
  506. if err != nil {
  507. http.Error(w, err.Error(), http.StatusBadRequest)
  508. return
  509. }
  510. _ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "collector": telem.Config(), "config": cur})
  511. default:
  512. http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
  513. }
  514. })
  515. mux.HandleFunc("/api/debug/audio-stutter/browser-summary", func(w http.ResponseWriter, r *http.Request) {
  516. w.Header().Set("Content-Type", "application/json")
  517. if r.Method != http.MethodPost {
  518. http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
  519. return
  520. }
  521. streamer := recMgr.StreamerRef()
  522. if streamer == nil {
  523. http.Error(w, "streamer unavailable", http.StatusServiceUnavailable)
  524. return
  525. }
  526. body, err := io.ReadAll(io.LimitReader(r.Body, 64*1024))
  527. if err != nil {
  528. http.Error(w, "read failed", http.StatusBadRequest)
  529. return
  530. }
  531. if len(body) == 0 {
  532. http.Error(w, "empty body", http.StatusBadRequest)
  533. return
  534. }
  535. var payload any
  536. if err := json.Unmarshal(body, &payload); err != nil {
  537. http.Error(w, "invalid json", http.StatusBadRequest)
  538. return
  539. }
  540. if err := streamer.AppendBrowserAudioSummary(payload); err != nil {
  541. http.Error(w, "persist failed", http.StatusInternalServerError)
  542. return
  543. }
  544. _ = json.NewEncoder(w).Encode(map[string]any{"ok": true})
  545. })
  546. }
  547. func newHTTPServer(addr string, webRoot string, h *hub, cfgPath string, cfgManager *runtime.Manager, srcMgr *sourceManager, dspUpdates chan dspUpdate, gpuState *gpuStatus, recMgr *recorder.Manager, sigSnap *signalSnapshot, eventMu *sync.RWMutex, phaseSnap *phaseSnapshot, telem *telemetry.Collector) *http.Server {
  548. mux := http.NewServeMux()
  549. registerWSHandlers(mux, h, recMgr)
  550. registerAPIHandlers(mux, cfgPath, cfgManager, srcMgr, dspUpdates, gpuState, recMgr, sigSnap, eventMu, phaseSnap, telem)
  551. mux.Handle("/", http.FileServer(http.Dir(webRoot)))
  552. return &http.Server{Addr: addr, Handler: mux}
  553. }
  554. func telemetryQueryFromRequest(r *http.Request) (telemetry.Query, error) {
  555. q := r.URL.Query()
  556. var out telemetry.Query
  557. var err error
  558. if out.From, err = telemetry.ParseTimeQuery(q.Get("since")); err != nil {
  559. return out, errors.New("invalid since")
  560. }
  561. if out.To, err = telemetry.ParseTimeQuery(q.Get("until")); err != nil {
  562. return out, errors.New("invalid until")
  563. }
  564. if v := q.Get("limit"); v != "" {
  565. if parsed, parseErr := strconv.Atoi(v); parseErr == nil {
  566. out.Limit = parsed
  567. }
  568. }
  569. out.Name = q.Get("name")
  570. out.NamePrefix = q.Get("prefix")
  571. out.Level = q.Get("level")
  572. out.IncludePersisted = true
  573. if v := q.Get("include_persisted"); v != "" {
  574. if b, parseErr := strconv.ParseBool(v); parseErr == nil {
  575. out.IncludePersisted = b
  576. }
  577. }
  578. tags := telemetry.Tags{}
  579. for key, vals := range q {
  580. if len(vals) == 0 {
  581. continue
  582. }
  583. if strings.HasPrefix(key, "tag_") {
  584. tags[strings.TrimPrefix(key, "tag_")] = vals[0]
  585. }
  586. }
  587. for _, key := range []string{"signal_id", "session_id", "stage", "trace_id", "component"} {
  588. if v := q.Get(key); v != "" {
  589. tags[key] = v
  590. }
  591. }
  592. if len(tags) > 0 {
  593. out.Tags = tags
  594. }
  595. return out, nil
  596. }
  597. func shutdownServer(server *http.Server) {
  598. ctxTimeout, cancelTimeout := context.WithTimeout(context.Background(), 5*time.Second)
  599. defer cancelTimeout()
  600. _ = server.Shutdown(ctxTimeout)
  601. }