Events
Most of what Keal shows changes without anyone asking it to: a VM finishes pausing, a compute node reports in, a backup completes half an hour after the request that started it returned. Those arrive in the browser as Server-Sent Events, pushed from the Perl backend rather than polled for.
The path an event takes
Anything in the backend that knows something changed calls updateUI, which
hands the event to postEvent in cgi/Stabile.pm. That posts it to Keal, which
fans it out to whichever browsers are entitled to see it.
Why it is not a poll
The backend knows more than any before-and-after comparison could tell. When a
backup finishes, steamExec reads the backup tool's output and says what
happened — Backed up 204.8 MB (214748364 bytes), or why it failed. A poll
would only ever see that the status changed.
It is also quick: posting an event to Keal costs a couple of milliseconds, and it reaches the browser at once instead of at the next reload.
Audiences
An event reaches only the people it concerns:
| Event | Who receives it |
|---|---|
| Owned by a user | That user |
tab: nodes |
Administrators |
Owned by irigo or common |
Administrators |
Admin status is resolved once per connection, not per event — these connections are long-lived, so the cost is paid once per browser tab.
Topics
Each event from the backend is published under stabile.<tab> —
stabile.images, stabile.servers — and the monitors' under
keal.monitors. A tab module subscribes to just its own resource:
onServerEvent('stabile.images', function (d) {
if (d.message) Keal.toast(d.message, 'info');
if (d.uuid && d.status) applyStatusUpdate(d.uuid, d.status);
});
One EventSource is shared by the whole app and multiplexed by topic
(events.js). A browser allows only a handful of concurrent connections per
origin, so a stream per feature would exhaust that budget.
Best effort, deliberately
Events are not persisted, not replayed after a reconnect, and not acknowledged. A browser that was disconnected for a few seconds has simply missed what happened.
Push is latency reduction, not the source of truth
Every consumer keeps its own periodic refresh — the tabs reload every minute or two regardless. That backstop is what makes a dropped event a delay rather than a permanently wrong screen.
Authentication
The publishers are backend processes, some running as root, with no ticket of
their own. They authenticate with the SHA-512 of the shared TKTAuthSecret —
the same hash the engine uses to prove itself to the registry.
A loopback check would be no substitute: Apache reverse-proxies this vhost, so
every request arrives from 127.0.0.1 regardless of where it came from.
Adding a producer
From Python, publish onto the hub directly:
from api import events
await events.publish(
"stabile.images",
{"uuid": uuid, "status": "unused", "message": "Done"},
events.Audience.user_scoped(user),
)
From Perl, call updateUI as the resource scripts already do — postEvent
happens underneath it, and nothing else is required.