// SPDX-FileCopyrightText: 2026 Echolot contributors // SPDX-License-Identifier: GPL-3.0-or-later package adminui import ( "encoding/json" "net/http" "time" ) // The read side of the dev relay (control/devtools.go submits, this reads back). It is here, in // the UI that already authenticates, rather than in a listener of its own — that is the lesson of // the beacon receiver it replaces, which was a separate unauthenticated service on port 443 of the // addresses reserved for measurement. // adbRow is one relayed endpoint as the API and the dashboard both see it. // // age_s is computed rather than left to the reader: the port it describes rotates every few // minutes, so how old the report is decides whether it is worth trying at all. type adbRow struct { Device string `json:"device"` Host string `json:"host"` Port int `json:"port"` DeviceName string `json:"device_name,omitempty"` Note string `json:"note,omitempty"` ReportedAt time.Time `json:"reported_at"` SourceIP string `json:"source_ip,omitempty"` AgeS int `json:"age_s"` } // adbRows reads the store's live endpoints (already newest first, already aged out). func (s *Server) adbRows() []adbRow { now := time.Now().UTC() eps := s.Store.ADBEndpoints() rows := make([]adbRow, 0, len(eps)) for _, e := range eps { age := int(now.Sub(e.ReportedAt).Seconds()) if age < 0 { age = 0 // a clock that ran backwards should read "just now", not negative } rows = append(rows, adbRow{ Device: e.Device, Host: e.Host, Port: e.Port, DeviceName: e.DeviceName, Note: e.Note, ReportedAt: e.ReportedAt, SourceIP: e.SourceIP, AgeS: age, }) } return rows } // adbEndpointsAPI is GET /admin/adb-endpoints — the developer's half of the relay. // // Authenticated exactly like the mint endpoint, so `curl -u admin:PASS` works from a script and a // signed-in browser session works without one. Admin-only: a LAN address and a debug port are the // operator's business, and a user's uploads are not made safer by handing out either. func (s *Server) adbEndpointsAPI(w http.ResponseWriter, r *http.Request) { if _, ok := s.apiAdmin(w, r); !ok { return } w.Header().Set("Content-Type", "application/json") rows := s.adbRows() if rows == nil { rows = []adbRow{} // an empty list, never null: a caller should not have to special-case it } _ = json.NewEncoder(w).Encode(rows) }