Wideband autonomous SDR analysis engine forked from sdr-visual-suite
Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

578 rindas
22KB

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