From a browser kiosk to a 3 MB TUI.
Part 9 · the living-room screen
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:
- The TV went blank after a while, every time right after the kiosk restarted.
- Ctrl+Alt+F2 did nothing. The console switched underneath, but the screen kept showing the page.
- A workaround made it worse. A hotkey daemon that stopped the kiosk for a login prompt and started it again lost the TV completely after a few round trips.
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.
| MEASURED | BROWSER KIOSK | TOWERVIEW |
|---|---|---|
| memory | ~1.6 GB | 3.0 MB |
| graphics memory | ~300 MB | none |
| on disk | Firefox, cage and 72 dependency packages | one 2.2 MB file |
| switch to a root login | did not work | Ctrl+Alt+F2, instant |
| signal drops | after every restart | none 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
- It started with a Raspberry Pi
- One Ubuntu box and a macvlan network
- Moving to Proxmox, one service per container
- Four cores to sixteen
- Sixteen days of backups that weren't
- Choosing a 70-watt GPU
- Twelve cores and most of a GPU
- Building the lab with an agent in the loop
- One GPU, three containers, one owner
- From a browser kiosk to a 3 MB TUI