Read-only status integration

Enabling it

EJABBERD_ENABLED = True
EJABBERD_HOST = "localhost"           # or a HUB_HOSTS name -- see "One shared server" below
EJABBERD_DOMAIN = "your-xmpp-domain"  # the vhost ejabberd serves, e.g. "pyobs.example.org"
EJABBERD_API_URL = "http://127.0.0.1:5281/api"

A module’s XMPP identity is its comm.user config key, resolved the same way this app already resolves acl: blocks (so it works whether comm: is defined locally, via {include}, or via a YAML anchor merge key). A module with no comm: block at all (e.g. a pure HTTP module) is skipped entirely everywhere below – there’s nothing for it to connect to, so there’s no “should be connected but isn’t” mismatch worth surfacing for it.

Where it surfaces

  • Dashboard – a summary tile (connected / registered counts, node status as a tooltip) alongside the existing Total/Running/Stopped/RAM/CPU tiles, plus a small filled-green “connected” / outlined-amber “not connected” icon per module row. The tile’s denominator is this installation’s own modules that have a comm_user and aren’t deactivated – not ejabberd’s fleet-wide registered-account count, which can include unrelated accounts (admin, other roles) that have nothing to do with any module here.

  • Module detail page (Overview tab) – connected-since/IP/connection type if live, or last-seen (with the actual disconnect reason, if any) if not, or “not a registered account” if comm.user doesn’t correspond to a real ejabberd account at all.

Both are gated on the module’s own running status: a stopped module never shows a “connected” state, even if another module happens to share its comm.user and is currently connected under that identity (a real, supported configuration – e.g. a _test copy of a module reusing a real module’s identity for testing).

Data layer

The primary mechanism is ejabberd’s HTTP admin API, mod_http_api – not ejabberdctl subprocess calls – since it’s roughly 50-60x faster per call (hits the already-running node directly instead of booting a fresh Erlang VM per invocation). ejabberdctl is kept as a documented fallback for hosts that haven’t done the ejabberd-side setup below yet.

Command

Returns

Used for

status

Node status string

Dashboard: is the XMPP backbone itself healthy

stats

A bare integer (registered/online users, uptime)

Dashboard summary tile

connected_users_info

List of connected sessions (JID, IP, connection type, …)

Cross-referencing against modules for the “connected” indicator

registered_users

List of registered account names

Sanity-checking comm.user against real accounts

user_sessions_info

Same shape as one connected_users_info entry

Module page: is this module’s identity connected, since when, from where

get_last

Last-seen timestamp + status, or a disconnect reason, or “not found”

Module page: “last connected 3h ago (stream reset by peer)”

check_account

Whether an account is registered at all

Module page: flag a comm.user that isn’t a real XMPP account

ejabberd-side configuration

Add an HTTP listener with mod_http_api, and a permissions grant limited to the read-only commands above:

listen:
  -
    port: 5281
    ip: "127.0.0.1"         # loopback only -- see security note below
    module: ejabberd_http
    request_handlers:
      /api: mod_http_api    # add to an *existing* listener's request_handlers if one's
                             # already on this port -- ejabberd allows one listener per port

modules:
  mod_http_api: {}

api_permissions:
  "console commands":
    from: [ejabberd_ctl]
    who: all
    what: "*"
  "pyobs-web-admin readonly":
    from: [mod_http_api]
    who:
      access:
        allow:
          - acl: loopback
    what:
      - "status"
      - "stats"
      - "connected_users_info"
      - "registered_users"
      - "user_sessions_info"
      - "get_last"
      - "check_account"

Reload ejabberd’s config afterward (ejabberdctl reload_config, or a restart if that doesn’t pick up a new listener). The what: list is a deliberate whitelist – leave it as-is; mod_http_api can also expose account-management commands (register/unregister/change_password) that must never be reachable through this read-only grant (see XMPP user management for how those are handled instead).

Security model

Access is IP-based, not credential-based – the only gate is acl: loopback in api_permissions; there’s no username/password or bearer-token layer on this endpoint at all. This is a deliberate, tested choice, not an oversight: both an OAuth bearer token and HTTP Basic Auth were tried and abandoned (OAuth failed with a missing dependency in one tested ejabberd build; Basic Auth kept returning an unrelated authorization error despite valid credentials). What this does and doesn’t protect against:

  • Does protect against any request arriving from outside the machine, over the network – confirmed by testing from a real non-loopback address, which was rejected even for commands in the explicit whitelist.

  • Does not protect against any other local process or user account on the same machine – the ACL can’t distinguish “pyobs-web-admin specifically” from “anything else on this box that can reach 127.0.0.1:5281.” This is an accepted tradeoff for a dedicated, single-purpose observatory control host, not appropriate for a shared or multi-tenant one.

One shared server, hub-aware

ejabberd is normally one server for the whole fleet, not one per host, so this isn’t a many-hosts-aggregate problem the way the ACL matrix is – it’s “delegate to the one host that has it.” If EJABBERD_HOST names a HUB_HOSTS entry instead of "localhost", every other instance in the fleet transparently proxies its ejabberd queries to that one host through the existing hub-token-authenticated proxy (see Hub mode) – rather than pointing EJABBERD_API_URL at a remote host’s IP directly, which would mean widening ejabberd’s own loopback-only ACL to accept a specific remote caller instead. Only the one host that actually runs ejabberd needs EJABBERD_API_URL pointed at a real instance; every other host just needs EJABBERD_HOST set to that host’s name.