Captive portal health

GET /api/bng/captive-portal/health                     read_only
GET /api/bng/captive-portal/preview                    read_only

The health endpoint performs a live probe of the configured portal on every call
— it is not cached — and maintains a short in-process ring buffer of recent
results.

{
  "configured": true,
  "portal_ip": "10.0.0.5",
  "url": "http://10.0.0.5:8088/health",
  "ok": true,
  "status_code": 200,
  "latency_ms": 12,
  "error": null,
  "consecutive_failures": 0,
  "history": [ { "ts": 1753000000.0, "ok": true, "status_code": 200, "latency_ms": 12, "error": null } ]
}

If no portal is configured the response is
{"configured": false, "ok": null, "consecutive_failures": 0, "history": []}
note that ok is null here and boolean elsewhere.

The success rule is broader than it might appear: any status from 200 through
499 counts as up.
A 404 means something is listening and speaking HTTP, which
is the question being asked. latency_ms is populated even on failure, measuring
time until the exception. An outage opens only after two consecutive failures,
which is what keeps a single dropped probe from raising an alarm.

An outage identifier embeds the outage start time, so each distinct outage cycle
gets a fresh identifier and correctly bypasses an acknowledgement of the previous
one. On recovery the outage is deliberately kept in the event feed with
severity: "info" and a "recovered (was intermittent)" summary, so an operator
must still acknowledge that it happened.

/captive-portal/preview is a same-origin HTML proxy of the portal's landing
page, provided so the UI can display the live portal in an iframe regardless of
whether the operator's browser can route to the portal address. It returns HTML,
not JSON: 400 if no portal is configured, 502 if the portal is unreachable,
and otherwise the upstream body and content type.

The Audit Timeline

An operational question — "what happened at 04:12 last Tuesday" — cannot be
answered from any single log on a BNG. The relevant evidence is distributed
across the controller's audit log, QuantumTouch's own event log, the systemd
journal for four services, and the captive portal's access log. The audit
timeline exists to merge them.