Summary
api_module_classes (GET /api/modules/classes/, modules/views.py:752-762) has no fleet-aggregating counterpart, unlike every other cross-host-consumed endpoint in this app. External callers (currently: pyobs-portal, for its script-builder module dropdown) only ever see the one host they're configured against — modules on other hub clients are invisible, with no config knob on either side to fix it.
Found while investigating a "web-admin only shows modules on itself, not hub clients" question, which turned out to be user error about which page (Dashboard is single-active-host by design; Fleet Overview already aggregates) — but surfaced this separate, real gap.
Current state
Three hub-interaction patterns already coexist here:
- Single active-host, session-driven proxying (
dashboard, module_detail, api_config, start/stop/restart). _active_host() (views.py:95) + _proxy() (views.py:99) forward to session["active_host"] via proxy.call() (modules/proxy.py:20-25). Fine for a human clicking around; useless for a caller with no session.
- Fleet-wide aggregating pages (
fleet_overview, ACL Matrix, All Logs, Users). Loop over [localhost] + settings.HUB_HOSTS, call each host's own raw endpoint, merge server-side (services.merge_acl_matrices, services.py:2444, is the pattern to copy).
- Raw, always-local, hub-facing endpoints, meant to be queried by another instance doing pattern 2's merge (
api_acl_matrix, api_module_classes, the comm-user-map endpoint). api_acl_matrix and the comm-user-map endpoint both have a pattern-2 page built on top of them (ACL Matrix, Users). api_module_classes does not — nothing in this app ever asks "what module classes exist across the whole fleet."
Confirmed live: our two-instance pair (south/monet acting as hub, south/frontend as client) already has matching HUB_HOSTS/HUB_TOKEN/HUB_CLIENTS secrets — the hub wiring itself isn't broken. Portal just has one WEBADMIN_URL and calls api_module_classes directly, so it only ever gets south/monet's own modules.
Proposed fix
- Add
services.merge_module_classes(per_host: list[tuple[str, dict]]), mirroring merge_acl_matrices, and either a new endpoint (GET /api/modules/classes/fleet/) or an opt-in query param (?fleet=1) on the existing one that loops [localhost] + HUB_HOSTS like fleet_overview does. Default behavior for existing callers stays local-only.
- Decide collision handling (same module name, different class, on two hosts): error vs. last-host-wins-with-a-warning vs. namespaced by host. Check whether ACL Matrix/Users already made an equivalent call and follow precedent.
- Update pyobs-portal (
pyobs_portal/api/webadmin.py:28-59) to call the new fleet endpoint once it exists, pointed at the hub instance (south/monet) — confirm its existing "portal" HUB_CLIENTS token already authorizes the new endpoint (should be free, same middleware).
Open questions
Work plan
Summary
api_module_classes(GET /api/modules/classes/,modules/views.py:752-762) has no fleet-aggregating counterpart, unlike every other cross-host-consumed endpoint in this app. External callers (currently: pyobs-portal, for its script-builder module dropdown) only ever see the one host they're configured against — modules on other hub clients are invisible, with no config knob on either side to fix it.Found while investigating a "web-admin only shows modules on itself, not hub clients" question, which turned out to be user error about which page (Dashboard is single-active-host by design; Fleet Overview already aggregates) — but surfaced this separate, real gap.
Current state
Three hub-interaction patterns already coexist here:
dashboard,module_detail,api_config, start/stop/restart)._active_host()(views.py:95) +_proxy()(views.py:99) forward tosession["active_host"]viaproxy.call()(modules/proxy.py:20-25). Fine for a human clicking around; useless for a caller with no session.fleet_overview, ACL Matrix, All Logs, Users). Loop over[localhost] + settings.HUB_HOSTS, call each host's own raw endpoint, merge server-side (services.merge_acl_matrices,services.py:2444, is the pattern to copy).api_acl_matrix,api_module_classes, the comm-user-map endpoint).api_acl_matrixand the comm-user-map endpoint both have a pattern-2 page built on top of them (ACL Matrix, Users).api_module_classesdoes not — nothing in this app ever asks "what module classes exist across the whole fleet."Confirmed live: our two-instance pair (
south/monetacting as hub,south/frontendas client) already has matchingHUB_HOSTS/HUB_TOKEN/HUB_CLIENTSsecrets — the hub wiring itself isn't broken. Portal just has oneWEBADMIN_URLand callsapi_module_classesdirectly, so it only ever getssouth/monet's own modules.Proposed fix
services.merge_module_classes(per_host: list[tuple[str, dict]]), mirroringmerge_acl_matrices, and either a new endpoint (GET /api/modules/classes/fleet/) or an opt-in query param (?fleet=1) on the existing one that loops[localhost] + HUB_HOSTSlikefleet_overviewdoes. Default behavior for existing callers stays local-only.pyobs_portal/api/webadmin.py:28-59) to call the new fleet endpoint once it exists, pointed at the hub instance (south/monet) — confirm its existing"portal"HUB_CLIENTStoken already authorizes the new endpoint (should be free, same middleware).Open questions
api_module_classes's "always local" docstring (referencing issue Add API endpoint returning module name -> class for external callers (e.g. pyobs-robotic-backend) #65) reflect a deliberate requirement, or just what shipped first? Check issue Add API endpoint returning module name -> class for external callers (e.g. pyobs-robotic-backend) #65 before building anything — portal may genuinely only need one host.api_module_classesthe only pattern-3 endpoint missing a pattern-2 aggregator, or are there others? Auditmodules/urls.py'sapi/*GETs against this to be sure.Work plan
api/*GET endpoints inmodules/urls.py, classify pattern 1/2/3, confirm scopemerge_module_classes+ new endpoint/param + collision rule, unit tests inmodules/tests.pysouth/monet+south/frontendpair