monk97.mewritingtv-dashboard

From a browser kiosk to a 3 MB TUI.

Part 9 · the living-room screen

2026-10-25 home lab series · part 9 of 9

The lab machine moved next to the living-room TV, which already had an Apple TV on one input. The idea was simple: switch to the other input and see, at a glance, whether everything in the lab is up. Getting there took one afternoon and two complete attempts. The first was a web page in a browser kiosk, which looked good and kept losing the signal. The second was a terminal UI written in Rust, which uses 3 MB of memory and has not dropped the picture so far.

NO SIGNAL

The card is the slot-powered RTX PRO 2000 from part 5. It has only mini-DisplayPort outputs, so the TV is on the end of a passive mini-DP (Thunderbolt) to HDMI cable. That cable had worked on a monitor. On the TV: nothing.

Two separate problems turned up. The first was software. Since the Proxmox install (part 2), the NVIDIA driver's display module had been blacklisted, so the only picture the host could ever make was the motherboard firmware's boot framebuffer: 1024×768, lit once at power-on, and blind to anything plugged in later. Many TVs refuse 1024×768 over HDMI outright.

ON THE HOST: CHECK WHAT IS DRIVING THE SCREEN

cat /etc/modprobe.d/*nvidia*            # blacklist nvidia_drm   <- there it is
cat /proc/fb                            # 0 simpledrmdrmfb     <- the firmware's framebuffer
for c in /sys/class/drm/card*-*; do echo "$(basename $c) $(cat $c/status)"; done

ON THE HOST: LET THE DRIVER OWN THE CONSOLE

echo 'options nvidia-drm modeset=1 fbdev=1' > /etc/modprobe.d/nvidia-drm.conf
echo nvidia_drm > /etc/modules-load.d/nvidia-drm.conf
mv /etc/modprobe.d/blacklist-nvidia-drm.conf /root/      # keep it, just in case
update-initramfs -u

With mode-setting on, the driver owns the text console and follows displays as they come and go. The second problem was physical. Through a passive adapter, this TV's "I'm here" signal (hot-plug detect) flickers. All four ports would read disconnected with the TV on and on the right input, then connected a few minutes later. Asking the GPU to look again usually found it:

echo detect > /sys/class/drm/card1-DP-3/status
cat /sys/class/drm/card1-DP-3/status     # connected

That became a small timer that probes again every 20 seconds whenever nothing reads connected. The proper fix is an active adapter, which makes its own signal instead of passing the TV's through. It is on the shopping list.

THE STATUS PAGE THAT LIED

While checking the public status page, the GPU services showed as down, and had for ten days. Nothing was broken. In part 8 the model server and the chat front end moved to new containers, but two monitors still pointed at the old addresses. They had been faithfully reporting an empty container ever since.

The fix kept the monitor names, so the public page and the history stayed intact, and changed only the targets. It also added pings for the new containers. The red stretch stays in the 90-day bars: it is what was measured, and filling it in would be inventing data. The lesson cost nothing to learn: when a service moves, its monitor moves in the same change.

FIRST TRY: A WEB PAGE IN A KIOSK

The obvious build was a full-screen browser on the host's console: cage, a compositor that runs exactly one window, around Firefox in kiosk mode. It started on the TV whenever a screen was connected. Uptime Kuma's own status page was too long for a TV and needed scrolling, so I wrote a single-screen page against the same status-page API: every service as a tile with a status dot, its 24-hour uptime and its last 40 checks, plus a "needs attention" panel. It fitted 1920×1080 exactly and looked good.

It also failed in three ways that turned out to share one cause:

Every hand-off between the compositor and the text console makes the GPU re-train the link to the display, and this TV, behind a passive adapter, often does not lock back on. Forcing the port "on" was worse: with no TV detected, the GPU sends DisplayPort signalling, which a passive adapter cannot turn into HDMI. The kiosk also cost about 1.6 GB of RAM and 300 MB of graphics memory to draw a status page.

SECOND TRY: TOWERVIEW

The text console itself had been reliable all along. When nothing takes the display away, nothing re-trains. So the dashboard moved onto the console: towerview, a terminal UI in Rust built on ratatui. It logs in automatically on the first console as a locked-down user and draws one screen:

TOWERVIEW, ONE FRAME (THE COMPACT 120×33 LAYOUT)

╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ arttower  ·  lab   ALL 24 UP                  ● 24 up   ● 0 down   ● 0 pending              Sun 25 Oct  15:19   ↻ 0s │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭ ● host ───────── up 2h 49m ╮╭ ● gpu ─────── RTX PRO 2000 ╮╭ ● storage · network ───────╮╭ ● guests ─── 14/14 running ╮
│ cpu   █░░░░░░░░░░░░░░   3% ││ util  ░░░░░░░░░░░░░░░   0% ││ root  █░░░░░  15% of 93.9G ││ kind  13 containers · 1 vm │
│ ram   █░░░░░░░ 10.7G/62.7G ││ vram  ░░░░░░░░░░░░ 0.1/16G ││ nvme  █░░░░░░  15% of 915G ││ state all running          │
│ load  0.45  2.04  1.86     ││ temp  37°C   6 W           ││ data  █░░░░░░  15% of 915G ││                            │
│ temp  51°C                 ││ model idle                 ││ net   ↓ 300 kb/s    ↑ 52 k ││                            │
│                            ││                            ││                            ││                            │
╰────────────────────────────╯╰────────────────────────────╯╰────────────────────────────╯╰────────────────────────────╯
╭ ● virtualisation ─────── 3 ╮╭ ● network & dns ──────── 3 ╮╭ ● compute & ai ───────── 5 ╮╭ ● storage & backup ───── 3 ╮
│ ● Proxmox VE      100% ■■■ ││ ● Reverse proxy   100% ■■■ ││ ● LLM inference   7.0% ■■■ ││ ● NAS             100% ■■■ │
│ ● Guests         99.9% ■■■ ││ ● DNS             100% ■■■ ││ ● Speech          100% ■■■ ││ ● NFS share       100% ■■■ │
│ ● Backups         100% ■■■ ││ ● TLS certs       100% ■■■ ││ ● Image gen       100% ■■■ ││ ● Files          98.8% ■■■ │
│                            ││                            ││ ● Chat            7.0% ■■■ ││                            │
│                            ││                            ││ ● Jupyter         100% ■■■ ││                            │
│                            ││                            ││                            ││                            │
│                            ││                            ││                            ││                            │
╰────────────────────────────╯╰────────────────────────────╯╰────────────────────────────╯╰────────────────────────────╯
╭ ● observability ──────── 3 ╮╭ ● media & home ───────── 2 ╮╭ ● remote access ──────── 5 ╮╭ ● needs attention ─────────╮
│ ● Uptime Kuma     100% ■■■ ││ ● Jellyfin        100% ■■■ ││ ● Browser        98.8% ■■■ ││ all quiet                  │
│ ● Netdata         100% ■■■ ││ ● Homebridge      100% ■■■ ││ ● IT tools        100% ■■■ ││ nothing failed in recent c │
│ ● Logs            100% ■■■ ││                            ││ ● Desktop VM     92.0% ■■■ ││                            │
│                            ││                            ││ ● Sign-in        98.8% ■■■ ││                            │
│                            ││                            ││ ● Docs            100% ■■■ ││                            │
│                            ││                            ││                            ││                            │
│                            ││                            ││                            ││                            │
│                            ││                            ││                            ││                            │
╰────────────────────────────╯╰────────────────────────────╯╰────────────────────────────╯╰────────────────────────────╯
╭ ● recent ───────────────────────────────────────────────────────────────────────────────────────────────── 14 events ╮
│ ● 13:58 started omarchy-desktop (108)    ● 12:31 started open-webui (113)    ● 12:31 started inference (112)         │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
  s root shell     r redraw     ctrl+alt+f2 root login     ctrl+alt+f1 back to towerview                                

It shows the same Uptime Kuma data as the web page, plus what a browser page could not easily see. Host CPU, memory, load and temperature come straight from /proc. GPU load, memory, temperature and power come from nvidia-smi, along with which model the model server has loaded. It also shows disk use, network throughput, the guest list, and a bottom strip of recent activity: backups and guest starts from the Proxmox task log, and pushes to this site's repository.

ON THE HOST: THE FIRST CONSOLE LOGS STRAIGHT INTO IT

# /etc/systemd/system/[email protected]/autologin.conf
[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin kiosk --noclear --noissue %I $TERM

# the kiosk user's login shell is the dashboard; if it exits, getty logs in again
usermod -s /usr/local/bin/towerview kiosk

# /etc/sudoers.d/towerview: guest lists and the task log, nothing more
kiosk ALL=(root) NOPASSWD: /usr/sbin/pct list, /usr/sbin/qm list, \
    /usr/bin/pvesh get /nodes/arttower/tasks --limit 50 --output-format json

The host has no Rust toolchain and does not need one. The binary is built in a throwaway rust:alpine container on the inference box, which already runs Docker, as a single static file. A --snapshot flag renders one frame as text with live data, which is how the layout was checked at the TV's exact size before each deploy (and how the frame above was made).

Two console details mattered. Terminus Bold at 12×24 gives a 160×45 grid at 1080p, readable from the sofa and roomy enough to breathe. And the Linux console draws bold text in the bright palette slot, so the first version's bold headings came out in colours I never chose. Now nothing is bold (the font already is), and the program paints the console's sixteen colours itself.

MEASUREDBROWSER KIOSKTOWERVIEW
memory~1.6 GB3.0 MB
graphics memory~300 MBnone
on diskFirefox, cage and 72 dependency packagesone 2.2 MB file
switch to a root logindid not workCtrl+Alt+F2, instant
signal dropsafter every restartnone so far

Ctrl+Alt+F2 now gives a normal root login and Ctrl+Alt+F1 comes back. Both are switches between two text consoles, which the kernel does without touching the link. Pressing s inside towerview opens a root shell on the spot, and exit returns to the dashboard. The browser setup is removed and archived with notes on why it failed. The web page still exists for any browser at home; it is just not what the TV shows.

WHAT IS LEFT

An active mini-DP to HDMI adapter, so the TV stops dropping its signal at all and gains 4K and sound. Beyond that, why a terminal UI beat a web page here is a bigger question than one TV, and it gets its own post.

ASK AN AGENT

PROMPT: MY SERVER WILL NOT DRIVE THE TV

My home server (Proxmox, NVIDIA GPU) shows no picture on a TV connected through a mini-DisplayPort to HDMI cable. Find out why with read-only checks first, and change nothing until I agree.

- Which kernel driver owns the console (/proc/fb, lsmod), and whether nvidia_drm is blacklisted or loaded without modeset.
- What each GPU output reports (/sys/class/drm/card*-*/status), and whether a re-probe (echo detect) changes it.
- Whether the cable is a passive or an active adapter, and what that means if I force the output on.
- What resolution the console uses, and whether a TV would accept it.
Then propose the smallest fix, and how to make it survive a reboot.

THE SERIES

  1. It started with a Raspberry Pi
  2. One Ubuntu box and a macvlan network
  3. Moving to Proxmox, one service per container
  4. Four cores to sixteen
  5. Sixteen days of backups that weren't
  6. Choosing a 70-watt GPU
  7. Twelve cores and most of a GPU
  8. Building the lab with an agent in the loop
  9. One GPU, three containers, one owner
  10. From a browser kiosk to a 3 MB TUI
← One GPU, three containers, one owner writing → press / for a prompt