Lesson 32 of 32 advanced 6 min read

Before this:Choosing your web stack

The GopherTrunk web dashboard

Key takeaways This is the whole module in one worked example. GopherTrunk’s engine decodes radio calls and emits them on an internal event bus; a Go back end exposes them two ways — a REST API for history and a real-time stream for live pushes — and a light front end renders them into a live table the instant a radio keys up. It’s a deliberately small stack matched to the job, and it exercises nearly every idea in this path. See the running project at gophertrunk.org.

Every idea in this module — client and server, APIs, real-time, security, deployment — has been a piece in isolation. This final lesson assembles them into one real thing: the dashboard that shows what a scanner is hearing, live. It’s a worked example, not new theory, so read it as a walk-through of how the parts click together.

The problem it solves

GopherTrunk is a headless decoding engine: it manages SDR hardware, follows trunked systems, and decodes calls — but it has no screen. Its output is a stream of events (a call started on this talkgroup, a site went active, audio was recorded) arriving unpredictably and continuously. A terminal is a poor place to watch that. The dashboard’s job is to turn that live stream into a glanceable web view anyone on the network can open in a browser — the human face on top of the engine.

The back end: engine to API

The engine already emits everything of interest on an internal event bus — one of the sinks in the architecture diagram, alongside voice and storage. The web back end, written in Go like the rest of the project, subscribes to that bus and exposes it to browsers. It presents two surfaces, because a dashboard needs both history and what’s happening now:

  • A REST API for state and history — recent calls, active talkgroups, system status — that a freshly opened page can fetch to populate itself.
  • A real-time stream that pushes each new call as the engine emits it.
GET /api/calls?limit=50     ->  200 OK   [ {call}, {call}, … ]   # history
GET /api/systems            ->  200 OK   [ {system, active} ]    # status
(live stream)               ->  {call}   pushed as each one is decoded

Because the engine is Go with typed channels, bridging the event bus to an HTTP handler and a stream is a small, natural amount of code — the back end is mostly a thin translation layer from internal events to web responses.

The front end: a live table

The browser side is deliberately light, because the job is a table that updates. When the page loads it does two things, exactly the pairing from the WebSockets lesson: fetch recent history once over the REST API to fill the table, then subscribe to the live stream so every new call appends at the top the moment it’s decoded.

// 1. Load history once so the table isn't empty on open
const history = await (await fetch("/api/calls?limit=50")).json();
history.forEach(renderCall);

// 2. Subscribe to the live stream — new calls appear instantly, no refresh
const live = new EventSource("/api/stream");
live.onmessage = (e) => renderCall(JSON.parse(e.data));   // prepend to the table

Each call becomes a row — talkgroup, time, duration, system — built by updating the DOM as data arrives. There’s no heavy framework here because the UI is simple enough not to need one; the stack was matched to the job rather than to fashion.

Where the module’s pieces show up

The dashboard is a checklist of the whole path in one place:

Seeing them together is the point: a web app isn’t any one of these ideas, it’s all of them cooperating around a clear job.

Quick check: how does the dashboard show both past calls and new ones the instant they happen?

Recap

  • The dashboard is the human-facing front end on top of GopherTrunk’s headless decoding engine, turning a live event stream into a glanceable web view.
  • The Go back end subscribes to the engine’s internal event bus and exposes it two ways — a REST API for history/status and a real-time stream for live pushes.
  • The light front end fetches history once, then subscribes to the stream, so new calls append to a live table the instant they’re decoded — no refresh.
  • The stack is small and deliberate — Go throughout, a simple front end, no heavy framework — a direct application of matching the stack to the job.
  • It exercises nearly the whole module at once: client–server, REST, real-time, the DOM, security, static vs. dynamic, and deployment.

Next up: keep the glossary handy, and take it live with Containers & Deployment.

Frequently asked questions

Why does a scanner even need a web dashboard?

Because the interesting output — decoded calls, active talkgroups, which sites are up — arrives continuously and is far easier to watch in a browser than a terminal. A dashboard turns GopherTrunk’s stream of events into a live, glanceable view anyone on the network can open, without touching the command line. It’s the human-facing front end on top of the decoding engine.

How does a call get from the radio to my screen?

The engine decodes a call and emits it on its internal event bus. The web back end subscribes to that bus, and pushes each new call to connected browsers over a real-time channel. The browser, which loaded recent history over the REST API when it opened, appends the call to the live table the moment it arrives — no refresh.

Is this a heavy front-end framework app?

No — and that’s deliberate. The job is a live table of calls, so the stack is intentionally small: Go on the back end (the whole project is Go), a REST API for history, a real-time stream for live updates, and a light front end. It’s a concrete example of the ‘match the stack to the job’ principle from the previous lesson.