Repository navigation
A live feed to the console over SSE, and what it should carry that the activity feed does not #534
robert-dean
started this conversation in
Ideas
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
A live feed to the console over SSE
Written: 2026-10-10, moved out of from-v1 § "The monitoring feed" when
that discussion closed. It was the one item there with no other home. Every claim about this tree
below was checked against main at be84ebc on that date.
State of the tree: no server-sent events anywhere.
@maroonedsoftware/serverfeedis declaredin
apps/api/package.jsonand imported by nothing insrc. The onlytext/event-streamin the APIis
modules/stream/icecast.eventfeed.client.ts, which is the station READING Icecast's feed, notserving one. The console learns everything by polling.
What v1 had
A realtime pub/sub bus streamed to the console over SSE, with a JSON polling fallback: progress,
status, errors and logs from the render pipeline, jobs, the director, health probes and the LLM gate.
The generic core was lifted out into
@maroonedsoftware/serverfeed, which is why this repositorydeclares it. v1 kept only thin adapters:
apps/api/src/modules/monitor/(aprovideMonitor/getMonitorpair mirroringprovideAppConfigStore, because the feed is constructed insetup.server.tsbefore the container exists so boot-time logs already reach the bus), the SSE andJSON routers, and a logger bridge.
Three fixes came with it, and any SSE endpoint here will need all three:
DB transaction. Here that is one entry in
apps/api/src/server/middleware/transaction.exemptions.ts,which already exists for exactly this reason ("requests that must NOT pin a pooled connection") and
already lists streaming responses and now-playing.
closeIdleConnections()+closeAllConnections()after start). Otherwiseserver.close()waits forever on an open SSEconnection, the old process never exits, it keeps the port, and the restart looks like a hung boot.
Nothing in
apps/api/srccalls either today, which is fine only because nothing holds aconnection open yet. Read
apps/api/CLAUDE.mdon the lifecycle before adding it.ctx.respond = false) so a client disconnect does not make Koa logERR_STREAM_PREMATURE_CLOSEas a spurious error event in the feed. Nothing here does that yeteither.
SSE auth is fetch()-based, keeping the Bearer header, because
EventSourcecannot send one.What already exists, so it is not built twice
The activity feed (
modules/activity,GET /activityonplatform.view) is the durable half ofwhat v1's bus carried. It is one time-ordered list over three sources, none copied into the others:
the station's own moments (
station_events, written with their sentence by whichever producer had tophrase the thing), a break's journey through the render pipeline (
segment_events: planted, beingwritten, written, being spoken, ready, could not be made), and the records that actually aired
(
play_history). Paged with a cursor of a moment AND a row id (activity.feed.ts), filterable by aseverity floor (
info,warn,fault). The console polls only its first page, every 15 seconds(
apps/web/src/api/activity.queries.ts,ACTIVITY_POLL_MS).Logs and traces have routes of their own on
platform.manage(data/contracts/station/logs.ck,traces.ck), deliberately a higher floor than the activity feed because a log is whatever someprocess chose to write, verbatim, and
logs.ckrecords a known case of a secret in one.Everything else the console shows live is a poll at its own cadence: the playout reading every
2 seconds (
playout.queries.ts), the station's air state every 5 (director.queries.ts), segmentsevery 5 but only while one is in flight (
segments.queries.ts), plus attention, checkup, casts,productions and releases on intervals of their own. About two dozen
refetchIntervals inapps/web/src/api.And there is no general event bus, deliberately.
apps/api/CLAUDE.md§ "How work is dispatched"explains why, and names the one narrow exception,
StationBus, for a producer and its subscriber onopposite sides of
modules.ts. A feed to the console is a SINK, not a bus: nothing in the stationmay wait on it or react to it. Whatever publishes into it has to be fire-and-forget in exactly the
way
StationBus.publishis (never throws, nothing waits), or one stalled browser tab becomes a costthe station pays.
What it would carry
Two different things, and they should not be confused:
pushed as they happen instead of polled. This changes latency and request count, not content.
The rows already have stable ids and a cursor, so a reconnect can resume from the last id it saw
and the JSON fallback is the existing route. This half is the cheap one and is worth having only
if the polling is measurably a cost: today it is several small reads every few seconds per open
console.
writes it, which v1's production preview showed), a render's progress, a production's beats.
segment_eventsrecords the STATE changes, not what happens between them.whether a preemption fired (
modules/llm/llm.gate.ts). Today that is visible only in traces,after the fact.
platform.manageonly, for the reasonlogs.ckgives.The design question is whether (2) is worth a long-lived connection, a lifecycle change and a
middleware exemption, or whether a few of its items are better as short polls against new read
routes. v1's answer was a bus, but v1 also had no activity feed, so most of what its bus carried is
now in (1).
What would need deciding first
latency, the polling is fine and this stays closed.
segments should not receive every log line.
platform.viewand logs areplatform.manage; a singlestream has to enforce both per event, not per connection.
Whether they would take this feed (which means the generated SDKs learning SSE) or stay on polls is
worth deciding before the event shapes are, because a
.ckcontract is how those apps get anything.Moved from from-v1 § "The monitoring feed", closed 2026-10-10.
All reactions