mirror of
https://github.com/pyMC-dev/pyMC_Repeater.git
synced 2026-11-03 11:52:22 +01:00
Scope answers were built into the MQTT payload and then discarded, so the web UI had nothing to show between cycles and no way to ask a single repeater. Adds a store for them, a read endpoint, and a single-target query. Migration 15 adds neighbor_scopes: one row per queried neighbour holding the last answer (`scopes`, `responded_at`) alongside the last query's outcome (`status`, `queried_at`). The two are kept apart deliberately -- a failed query updates the outcome but leaves the answer in place, because the responder rate-limits anonymous replies to 4 every 3 minutes and one timeout is weak evidence that a neighbour's scopes changed. An empty answer is stored as a real answer: it means the neighbour serves unscoped traffic only. A row is written for any query the node attempted. `timeout` alone cannot carry that: the sweep reports it both for a neighbour that was asked and stayed silent and for one it never reached, which is what ScopeResult.transmitted separates. `send_failed` is recorded too even though nothing reached the air -- it was attempted and the duty cycle refused, and skipping it left a repeater that keeps refusing reading as "never queried" however often it was asked. Scope rows follow their neighbour out of the database, on all four paths that delete adverts: the two explicit deletes, the 6-hourly retention cleanup, and a purge of the adverts table. Without the last two the table grew without bound and a purge left scope counts on screen for repeaters no longer listed. GET /api/neighbor_scopes serves the table; it stays separate from the paginated advert queries, which are read per contact type on every page load. POST /api/query_neighbor_scopes asks one neighbour now. Unlike publish_neighbors it holds the request open for the reply, since the response window is normally its 5 s floor; past 45 s it returns and leaves the query running so a late answer still lands rather than throwing away spent airtime. It returns the stored view, not the raw result, so a failed query cannot tell the client to forget scopes the database still holds. Nothing is published -- the periodic cycle owns the topic. A query and a cycle must not collide over the scope helper. A cycle holds it for its whole run including the discovery window, so a query refuses while one is active; a cycle defers while a query is in flight; and if the two still race, the cycle abandons the pass on the short retry delay instead of publishing a table with every scope missing or dying and re-spending its discovery broadcast. Queries are tracked so shutdown cancels them rather than transmitting through teardown. The endpoint needs json_in: it is not on globally for /api and cherrypy's Request has no `json` attribute without it, so reading the body would have failed on every real request. A test asserts the decorator rather than trusting a fabricated cherrypy.request, which is how it went unnoticed.