feat(service): add passive peer-reachability probe handler

RunPeerReachabilityProbe is the post-migration replacement for the
active swUpdateUrl round-trip: register the device IP with the
in-process observer, nudge :8090/swUpdateCheck, and wait for any
inbound from that IP. No device-state mutation. Any inbound counts
as proof — on a migrated speaker, DNS interception routes the
daemon's outbounds through this service regardless of which URL it
resolved internally, so reachability reduces to "did the device
dial us at all."

PeerHit and the abstract observer interface live in setup alongside
the probe logic; handlers.peerObserver implements the interface and
the existing observer files now import from setup.

Route: POST /setup/peer-probe/{deviceId}. Timeout: 30s, surfaced as
result.ElapsedMs so the budget can be tuned from real data. The
pre-flight orchestrator gains the branch in the next commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tobias Gesellchen
2026-05-11 20:31:24 +02:00
co-authored by Claude Opus 4.7
parent dc924e351c
commit d74bb9b5ca
7 changed files with 330 additions and 22 deletions
+6
View File
@@ -878,6 +878,12 @@ func setupRouter(server *handlers.Server) *chi.Mux {
// because firmware may append path components (e.g. /index.xml).
r.Get("/probe/{token}", server.HandleProbeInbound)
r.Get("/probe/{token}/*", server.HandleProbeInbound)
// Passive peer-reachability probe. Registers a device IP with the
// in-process observer, nudges :8090/swUpdateCheck, and waits for
// any inbound from that IP. Used post-migration where the daemon
// caches its swUpdateUrl at boot and the active round-trip can't
// reach it without a reboot.
r.Post("/setup/peer-probe/{deviceId}", server.HandlePeerProbe)
r.Get("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
r.URL.Path = "/media/favicon-braille.svg"
server.HandleMedia()(w, r)