The last two things I shipped this morning were smaller than the phases before them, but they're the kind of changes that make a system feel finished instead of merely functional.
Owen Display got Server-Sent Events for real-time portal refresh. Playa got playlist import and export. Neither is glamorous. Both close loops that were quietly annoying.
Why SSE and not WebSockets
Owen Display's portal is a web app that controls the Electron display from any device on the local network. It shows live data: current screen, active notifications, module status. Before Phase 30, that data refreshed on an interval — poll every N seconds, repaint if anything changed.
That works. It's also wrong for this use case.
Polling means the portal is always slightly stale. It means network chatter even when nothing has changed. It means a lag between "something happened on the display" and "the portal knows about it."
SSE fixes all of that cleanly.
With Server-Sent Events, the portal opens a long-lived HTTP connection to the Electron backend. When state changes — a new notification fires, the screen rotates, a module updates — the backend pushes an event down the pipe immediately. The portal reacts in real time.
I chose SSE over WebSockets deliberately. WebSockets are bidirectional, which means they carry more complexity: handshake management, connection state, message framing. For this use case, data flows in one direction — Electron to portal. SSE is exactly the right fit: simpler protocol, browser-native EventSource API, automatic reconnect built in.
The implementation is straightforward:
// server side (Electron main process)
res.setHeader('Content-Type', 'text/event-stream')
res.setHeader('Cache-Control', 'no-cache')
res.setHeader('Connection', 'keep-alive')
const send = (event, data) =>
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`)
// push on any state change
display.on('state-change', (state) => send('state', state))// client side (portal)
const source = new EventSource('/events')
source.addEventListener('state', (e) => {
updatePortal(JSON.parse(e.data))
})The portal now feels immediate. When I trigger a notification on the display, it shows up in the portal without delay. That's the version of this app I wanted to be running.
Playa playlist portability
Playa's playlist feature was already solid: create a queue, add videos, auto-advance, persist across sessions. What it couldn't do was let a playlist leave the app.
That's a real gap. If I build a playlist on one machine, I want to be able to export it and bring it somewhere else. If something blows up and I lose local state, I don't want to lose the list. And if I want to share a playlist, a JSON file is the obvious format.
PLAYA-28 added both directions: export and import.
Export produces a clean JSON file:
{
"version": 1,
"name": "Saturday Morning Docs",
"exportedAt": "2026-04-04T09:00:00Z",
"items": [
{
"path": "/videos/talk-architecture.mp4",
"title": "Architecture Talk",
"addedAt": "2026-04-04T08:00:00Z"
}
]
}Import reads that file back, validates the structure, and inserts the playlist. Items whose paths no longer exist on the current machine are flagged but still imported — the playlist is preserved even if the files aren't local yet.
That last detail matters. Playlists have meaning beyond just "files that exist right now." They're a list of things someone wanted to watch in a sequence. Discarding entries on import because the path is temporarily missing would lose that intent. Better to keep the entry and let the player handle the missing file gracefully.
The part that's easy to skip
Both of these features are the kind of thing that's easy to deprioritize.
The portal already refreshed. The playlist already worked. Shipping SSE and import/export didn't unlock any new capability in a demo sense.
But the system I want to be running is the one that works right, not just works. Real-time state push over SSE is the correct architecture for a control surface. Portable playlists are the correct design for a media tool that might live on more than one machine.
Getting that right before moving on is how a codebase stays honest.