I had an old iMac that couldn't update anymore. Not that I wanted a new one. The OS was stuck on whatever version Apple stopped supporting, and every browser on it was one security patch behind where you needed to be. It sat on my desk, not quite obsolete, not quite useful, taking up space.

You probably have a machine like that somewhere. A laptop from 2015. A mini PC you told yourself you'd set up as a media server someday. They occupy that middle ground between trash and treasure where they cost too much to throw away and do too little to justify keeping.

This is the story of how that iMac became a full-screen tactical weather command center. A live thermal map of Florida. A newsroom of five voices reading real data out loud. A weather-reactive flamingo drawn in braille. A built-in radio playing original music. All running in a terminal, no API keys, no accounts, nothing to configure. And how building it taught me more about systems than any tutorial ever did.

You can find the whole project on GitHub: github.com/mikjgens/weatherDash.

The full weatherDash command center running live: a thermal map of Florida, a global seismic map, crypto market tape, aircraft radar, world clocks, and a weather broadcast typing out live

WHAT'S INSIDE
  • The old machine problem. What you can build when you stop waiting for the right tools.
  • A thermal map of Florida drawn in text. Inverse-distance-weighted temperature interpolation, half-block glyphs, and scanline-filled GeoJSON coastlines.
  • A broadcast network with no AI-written scripts. Five anchors, threshold ladders, and the rules that keep them from sounding like robots.
  • A braille flamingo that reads the sky. An animated scene with a logic tree, a deterministic pose engine, and a pink bird with an umbrella.
  • The radio I built to score it. A Winamp-style player with a fake spectrum analyzer and 15 original tracks.
  • The fix nobody asked for. Why terminal borders doubled and what it took to make them line up.
  • The architecture that holds it together. A signal core, a snap grid, and the resilience net that keeps every panel alive.

The Old iMac That Couldn't Update

The machine was a late 2013 iMac. 27 inches. Still a gorgeous display. But macOS had left it behind somewhere around Catalina, and by 2025 even Firefox was getting patches I couldn't install. It was a computer that could no longer safely touch the internet. I was about to recycle it.

But here's the thing. It ran a terminal. And it could run Node.js. And Firefox, out of date as it was, could still load a web-based AI chat. So I started pasting code back and forth. I'd ask the AI to generate a piece of a terminal dashboard. Copy it into a file. Run it. See what broke. Paste the error back. Fix. Repeat.

I didn't know what I was doing. I'm not a career developer. I've spent most of my working life on the consulting and marketing-tech side. The terminal was something I opened to run a Node script someone else wrote. I didn't know what git add did. I'd never refactored a file. Every step was learning.

Here is what that loop actually looks like. I asked for a panel that would show Florida as a temperature field. The AI generated a PNG approach using canvas. That wouldn't work in a terminal. So I said no, it has to be pure text characters. It generated a character grid with ASCII shading. That looked terrible. So I asked for Unicode half-blocks. It generated the math for that but did the interpolation wrong. I caught it because I know what Florida temperatures look like and the map was showing Miami cooler than Tallahassee. I pasted the error back. It fixed the weighting. Round and round like that for weeks.

Late nights. The iMac glowing in a corner of the room. Qwen sessions. DeepSeek sessions. One single file that kept getting bigger because I didn't know you were supposed to split things up. I was treating the AI like a senior developer who never got tired, and I was the intern asking stupid questions until the stupid questions started making sense.

Eventually I moved the code to my MacBook to finish it with Claude Code. But the iMac was the crucible. It proved that you don't need the newest hardware or the right setup. You need a problem you care about and the willingness to sit in the confusion until it clears.

What It Actually Does

Here is what runs on screen. Every piece of it is open source and on GitHub at github.com/mikjgens/weatherDash.

The whole thing runs in a macOS terminal at 271 by 75 characters. It targets a full screen, set-and-forget on a big display. I mirror it to the TV. It looks like a Bloomberg terminal crossed with a 1990s ops room, and that was exactly the goal.

Panel Data Source Key Detail Refresh
FL Theater 23 Open-Meteo city sensors Half-block Unicode rendering, 20-stop color ramp, IDW interpolation Real-time
Broadcast Newsroom NWS thresholds, Google News RSS, CoinGecko 5 deterministic anchor voices, zero LLM generation On demand
Live Sky WMO weather codes + wind data 512x512 braille pixel canvas, deterministic pose engine Real-time
Vibe Machine VLC rc interface + 15 original tracks True-shuffle queue, fake FFT spectrum analyzer Continuous
Seismic Map USGS earthquake feed Magnitude-colored markers, global plate boundaries Continuous
Warning Siren NWS alerts API Severity-ranked dossiers with countdown timers Continuous
Market Watch CoinGecko crypto prices Two-page split-flap rolling animation Continuous
Aircraft Radar Community ADS-B feeds Per-aircraft card with altitude, speed, origin Continuous
World Clocks System time Six time zones Continuous
Intel Feed System events + freshness probes Always-on log with green/red pulse indicators per panel Continuous

The FL Theater: A Live Thermal Map of Florida

This is the centerpiece. The entire state of Florida drawn as a solid temperature field, glowing from indigo in the Panhandle to red in Miami. Every pixel inside the coastline is colored by inverse-distance-weighting from about 23 live city sensors.

The FL Theater: Florida drawn as a temperature field with a Clermont forecast header and statewide city rankings

The rendering pipeline goes like this. The Florida coastline GeoJSON is projected onto a pixel grid with aspect correction. A scanline fill using the even-odd rule paints a mask of every pixel inside the state. For each interior pixel, the temperature is interpolated from the 23 city probes using a weighted average where weight equals 1 over distance squared plus a fudge factor. The result maps to a 20-stop color ramp from 100 degrees down to 43.

But here is the trick that made it look good. A terminal character cell is effectively one pixel tall, and Florida is tall and narrow. To get twice the vertical resolution, each character cell carries two independently colored pixels using half-block Unicode glyphs. The top pixel goes in the foreground color. The bottom pixel goes in the background color. The map renders at twice the height the terminal gives you. It looks like a real heat map because it is one.

Above the map, a Clermont forecast header packs the current conditions. Below it, an hourly forecast strip with color-coded temperature, blue-scale rain probability, and cloud cover glyphs. Along the bottom, a running ranking of the hottest and coolest cities in the state, sorted live.

The Broadcast Newsroom: Five Anchors, Zero AI Scripts

This is the part I'm most proud of. Press B for a weather broadcast. A voice starts reading the live conditions. Press I for Central Florida news. Press K for the crypto market. Press T for the full rundown.

Here is what matters. Not a single word is written by AI. Every line is a number falling into a band, exactly how broadcast meteorology has worked since the radio age. The dew point comfort scale. The Beaufort wind scale. The Douglas sea state. The old mariner's barometer rules. These are real thresholds that real people wrote down.

Scale What It Measures Output Type Origin
Dew Point Comfort range (55°F comfortable, 65°F sticky, 75°F oppressive) Humidity description Meteorological convention
Beaufort Wind Wind speed 0-12 (calm to hurricane) Wind conditions + practical effects Royal Navy, 1805
Douglas Sea State Wave height 0-9 (glassy to phenomenal) Marine conditions Met Office, 1920s
Barometer Trend Rising, falling, or steady over 3 hours Pressure forecast + storm likelihood Mariner tradition

The engine lives in ui/weatherNarrative.js. It's a deterministic system. Temperature goes in. Sentences come out. No API calls. No language models. No hallucinations. If a sensor is missing, the sentence that needs it is simply dropped. A short confident brief beats a long one full of holes.

There are five characters. Alex reads the inland Clermont forecast. Samantha covers the west coast marine conditions from the Gulf. Lee, the east coast anchor, has full Florida Man energy. He listens to what Alex and Samantha just said, scores their reactions by spiciness, and leads with the best one. He also reads the rest of the dashboard. He knows about the earthquake that just hit. He knows what Bitcoin is doing. He breaks the fourth wall, and it works.

Walter is the news desk. He reads Central Florida headlines from Google News with the gravitas of a man who has seen things. Penny is the market desk. She reads the crypto tape with bull and bear color. When an asset is up 8 percent, she says it's "absolutely ripping." When it's down 8 percent, it's "getting absolutely hammered."

Every character's macOS voice is auto-resolved from the installed list. No hardcoded voice names that might not exist on your machine.

The contrast with an LLM-based approach is instructive. If I had used a language model to generate the broadcast script, every run would produce different wording. It would be unpredictable. It might embellish. It might hallucinate a wind speed if the sensor was down. The threshold-ladder approach is the opposite. It is boring. It is predictable. It is auditable. You can trace any sentence back to the reading that produced it. That is the right kind of reliability for a weather dashboard. You do not want your broadcast to be creative. You want it to be true.

The Live Sky: A Weather-Reactive Braille Flamingo

In a small box on the left side of the screen, there is a lakeside scene drawn entirely in braille characters. A pink flamingo stands in the water. The sky changes with the weather. The lake ripples. The flamingo reacts.

The scene uses a custom pixel-to-braille renderer in ui/brailleCanvas.js. Each braille cell is a two-by-four dot matrix. The canvas gives you a 512-by-512 pixel buffer at terminal scale. Color is achieved through a layer map where each pixel carries both a brightness value and a layer ID.

The flamingo's pose is a pure function of the scene and the frame counter. No randomness. His head sways with a sine wave. He alternates which leg he tucks every 90 frames. He blinks every 88 frames. When it rains, he pops a yellow umbrella. When it storms, he hunkers down with both feet planted. When the wind is high, he leans into it by up to 2.2 pixels.

The logic tree maps WMO weather codes to scenes:

WMO Code Condition Scene Flamingo Behavior
0 Clear sky Sunny, bright palette Default pose, normal sway
1-3 Partly cloudy Light cloud cover Default pose, slightly slower sway
4-5 Overcast Muted gray sky Reduced animation speed
45-49 Fog Low-contrast fog overlay Stationary, head tilted
50-59 Drizzle Light rain particles Yellow umbrella deployed
60-69 Rain Heavy rain + darker sky Umbrella, hunkered posture
95-99 Thunderstorm Dark sky + lightning flash Both feet planted, no sway

There is no snow scene because Clermont, Florida has never seen snow, and the whole logic budget went to wind expression.

I've watched this flamingo for hours. It is strangely calming. Knowing that every pixel has a deterministic reason for being where it is.

The Vibe Machine: A Radio for the Dashboard

A dashboard this dense needs a soundtrack. So I wrote 15 original tracks and built a Winamp-style player to play them.

The Vibe Machine radio panel: purple spectrum analyzer with scrolling track title

The radio module in ui/radioModule.js spawns VLC behind the scenes with its rc interface. A true-shuffle queue reshuffles and loops forever. End-of-track detection works by polling VLC's labeled status reply for the stopped state. The spectrum analyzer is decorative but convincing. Real FFT of VLC's output is not possible, so it's modeled with per-bar attack and decay. Fast attack, slow decay, classic analyzer feel.

The tracks are mine. I wrote them with Suno, styled after the smooth, unbothered music the Weather Channel used to play under the local forecast in the 90s. The kind of music that makes you feel like everything is going to be fine even when a hurricane is coming.

When a broadcast starts, the music ducks gently. When the broadcast ends, it comes back up. The hourly chime does the same. Nothing ever talks over anything else.

The Rest of the Board

Beyond the main features, the dashboard fills every inch of the 12 by 12 grid. A global seismic map plots earthquakes from the USGS feed with magnitude-colored markers. The FL warning siren pulls live NWS alerts and shows severity-ranked dossiers with countdown timers. The market watch flips between two pages of crypto prices with a mechanical split-flap rolling animation. The aircraft radar pulls live ADS-B data from community feeds and shows a per-aircraft analysis card with altitude, speed, and origin. World clocks track six time zones. An always-on intel feed logs what the system is doing. Every panel has a small pulsing dot that is green when the feed is fresh and red when it's gone stale, so you can tell live numbers from frozen ones at a glance.

The Systems Behind the Scenes

The visible features are one thing. The invisible architecture is the part that took the longest to get right.

The Snap Grid: Fixing a Bug Nobody Talks About

Blessed-contrib's grid is great, but it has a cosmetic problem. When you dock panels next to each other, their borders sometimes double up. You get these ugly two-pipe seams between panels. The grid sets each panel's position as independent percentage strings, and blessed rounds each one separately at render time. A panel's right edge almost never lands on the same column where its neighbor's left edge lands. Dock borders can only merge borders that occupy the same cell, so it cannot fix the gap.

The fix was writing a drop-in replacement in core/gridLayout.js. Instead of percentages, it resolves the 12 by 12 grid to shared integer gridlines once for the real screen size. Every panel that meets a neighbor at the same boundary computes the same integer column. Their borders land on the same character. Dock borders merges them cleanly into proper T-junctions. I proved it seam-free with a test harness that scans for doubled borders. Zero seams at any size.

I spent more time on this than I did on the flamingo. That is the kind of engineering I respect. The invisible fix that nobody will ever notice unless it breaks.

The Signal Core: A Nervous System for the Dashboard

Each module publishes its latest headline to a shared event bus in core/store.js. This is the signal core. When the seismic module detects an earthquake, it publishes the magnitude and location. When the radar module finds an aircraft overhead, it publishes the callsign and altitude. Lee, the weather anchor, reads all of these signals when he comes on air. He knows the whole board's pulse. That is not magic. That is a shared state bus and modules that are designed to publish what they know.

The signal core also drives the weather scene. The live sky reads the current conditions from the store and adjusts the flamingo and the environment in real time.

The event bus is a simple pub/sub pattern. The radio module subscribes to broadcast status so it knows to duck the volume. The intel feed subscribes to every key so it can log state changes. The whole system talks through one file, 80 lines of JavaScript, no dependencies.

The Resilience Net: Keeping It Alive for Days

This dashboard is built to run for days on end without attention. Every feed has a request timeout and backs off on errors instead of hammering. A global uncaught exception handler catches any single module's crash and logs it to the intel feed. One bad frame can never take the whole dashboard down.

The broadcasts and the hourly chime coordinate so they never double up. The radio ducks cleanly under anyone speaking and always comes back up. When you quit, every spawned child process is killed. Nothing keeps talking in the background.

Smart CSR is disabled in blessed because partial-width panels share rows. The default scrolling optimization would mangle border characters on scrolling panels. The performance cost of disabling it is negligible because blessed still only writes changed cells.

These are the kind of details you only learn from running something for real. Not from a tutorial. From watching it run for a week and seeing where it breaks.

What It Takes to Run It

Clone the repo. Run npm install. Run node app.js. That is it. There are no API keys. No accounts. No environment variables. Every data feed is a free, keyless public source. USGS for earthquakes. The National Weather Service for alerts. Open-Meteo for weather and marine data. CoinGecko for crypto. Community ADS-B feeds for aircraft. Google News RSS for headlines.

You need a Mac because it uses built-in tools for audio and speech. You need Node.js 18 or newer. You need VLC only for the radio. You need a roomy terminal. It targets 271 by 75 characters, but it adapts to whatever you have. Below about 200 by 54, it shows a friendly message asking you to enlarge the window instead of rendering something broken.

Key Action
P Play / pause radio
B Weather broadcast (Alex + Samantha + Lee)
I Central Florida news (Walter)
K Crypto market report (Penny)
T Full rundown (all five anchors)
A / ? System briefing (explains every panel and key)
Escape Quit (kills all child processes)

The whole thing is meant to be set and forgotten.

What I Learned

I started this project not knowing how to refactor a file. I didn't know what an event bus was. I'd never written a Node.js application that spanned more than one file. I learned every system in this dashboard by needing it. Not by studying it. By needing it and figuring it out.

The old iMac that started this journey is still running. It sits in the corner with the dashboard on screen. When I walk past it, the thermal map is still glowing. The flamingo is still standing in the water. The radio is still playing. It has been running for weeks without a restart. That machine that could not update its browser turned out to be perfectly capable of running a live data center for a weather command post. You know, it just needed a job that matched what it could still do.

The principle is this. You do not need the newest hardware, the best setup, or the right background. You need a problem you care about enough to sit in the confusion until it clears. That iMac taught me that better than any book could.