Four sources, one shape

Source Origin Default
qt-events QuantumTouch's own event log and its archives On
controller-api The BNG controller's audit log On
systemd-journal journalctl across the BNG service units On
portal-access Captive portal HTTP access lines Off — opt in

Every entry from every source is normalised to the same eight fields:

{
  "ts": 1753000000.5,
  "source": "qt-events",
  "actor": "jdoe (session:9f8b2c14…)",
  "action": "event:qt_config_change",
  "summary": "Plan 'Silver-20M' updated",
  "detail": "rev_id=4412, kind=config_write, severity=info",
  "related_event_id": null,
  "raw": { "…the untouched original source record…" },
  "entry_id": "a1b2c3d4e5f6a7b8"
}

raw is what makes lossy normalisation safe. The original record is preserved
verbatim on every entry, so no information is destroyed by the projection and a
client can always fall back to source-native fields.

Actions are namespaced by source — event:<action>, controller:<METHOD> <path>,
service:{lifecycle|health|access|log}, portal:access — so a consumer can filter
by origin without inspecting the source field.

entry_id is a content-derived hash of the timestamp, source, and summary. It is
not persisted; it is recomputable from any feed of the same data, which is
what makes it usable as a cursor without server-side state.