package localapi import ( "context" "errors" "net/http" "time" "gitea.dooplex.hu/admin/felhom-agent/internal/linetest" ) // LineTestReader is the R-1247 weekly upload line test (internal/linetest.Tester) as the local API reads it. type LineTestReader interface { Value() (mbps float64, at time.Time, ok bool) } // LineTestRequester is the R-1250 half: a due test the uplink never lets start, and a test on request. The Tester // satisfies it; a reader without it answers waiting_for_quiet:false and 503 on the POST. type LineTestRequester interface { WaitingForQuiet() bool RunNow(ctx context.Context) (mbps float64, runs []float64, err error) } // LineTestResponse is GET /host/line-test. Present=false: never measured, or the job is not wired (no tunnel, disabled, // or an agent before the route answers 404 — the controller reads all three as „unknown"). type LineTestResponse struct { Present bool `json:"present"` Mbps float64 `json:"mbps,omitempty"` MeasuredAt string `json:"measured_at,omitempty"` // RFC3339 UTC Method string `json:"method,omitempty"` // "iperf3-tunnel": a 10-second upload to ep0 inside the tunnel // WaitingForQuiet: a measurement is due and could start now, but the uplink has not been quiet for 10 minutes // (R-1250). The controller with a seeding torrent app then holds its limit and POSTs /host/line-test/run. WaitingForQuiet bool `json:"waiting_for_quiet,omitempty"` } // LineTestRunResponse is POST /host/line-test/run. OK=false with Refused: nothing ran (Refused is linetest.Refusal's // code, or "server-busy"); OK=false without it: the runs failed. type LineTestRunResponse struct { OK bool `json:"ok"` Mbps float64 `json:"mbps,omitempty"` RunsMbps []float64 `json:"runs_mbps,omitempty"` Refused string `json:"refused,omitempty"` Error string `json:"error,omitempty"` } // SetLineTest wires the weekly line test (main.go builds it after the local API server). func (s *Server) SetLineTest(r LineTestReader) { s.lineTest = r } func (s *Server) handleLineTest(w http.ResponseWriter, r *http.Request, vmid int) { if s.lineTest == nil { writeOK(w, LineTestResponse{Present: false}) return } var waiting bool if q, ok := s.lineTest.(LineTestRequester); ok { waiting = q.WaitingForQuiet() } mbps, at, ok := s.lineTest.Value() if !ok { writeOK(w, LineTestResponse{Present: false, WaitingForQuiet: waiting}) return } s.logger.Debug("local-api: /host/line-test served", "vmid", vmid, "mbps", mbps, "measured_at", at.UTC().Format(time.RFC3339), "waiting_for_quiet", waiting) writeOK(w, LineTestResponse{Present: true, Mbps: mbps, MeasuredAt: at.UTC().Format(time.RFC3339), Method: "iperf3-tunnel", WaitingForQuiet: waiting}) } // handleLineTestRun is POST /host/line-test/run (R-1250): a test on request, synchronous (about 55 s). The answer is // always 200 with the outcome in the body, except 503 when no line test is wired. func (s *Server) handleLineTestRun(w http.ResponseWriter, r *http.Request, vmid int) { q, ok := s.lineTest.(LineTestRequester) if s.lineTest == nil || !ok { writeErr(w, http.StatusServiceUnavailable, "line test not configured") return } s.logger.Info("local-api: line test requested", "vmid", vmid) mbps, runs, err := q.RunNow(r.Context()) var ref *linetest.Refusal switch { case err == nil: writeOK(w, LineTestRunResponse{OK: true, Mbps: mbps, RunsMbps: runs}) case errors.As(err, &ref): s.logger.Info("local-api: line test request refused", "vmid", vmid, "reason", ref.Code, "detail", ref.Msg) writeOK(w, LineTestRunResponse{Refused: ref.Code, Error: ref.Msg}) case errors.Is(err, linetest.ErrBusy): writeOK(w, LineTestRunResponse{Refused: "server-busy", Error: err.Error()}) default: writeOK(w, LineTestRunResponse{Error: err.Error()}) } }