A touch-first, multi-node homelab console for physical Linux TTY displays.
It is designed for small directly attached displays — the reference setup is a 7-inch 1024×600 panel running as a 64×18 virtual console — with no X11, Wayland, browser or desktop environment required.
The application is built with Textual and
reads the touchscreen directly through Linux evdev.
Current release: v5.0.0. Read-only by design.
- Multiple nodes — monitor the local TouchRack host and remote Linux SBCs from the same touch-first interface, with a selector paged specifically for the 64×18 physical console.
- Host — CPU, memory, disk and temperature at a glance.
- Services — curated checks from containers, systemd, CRUX, HTTP or TCP, paged at up to 12 services per screen on the reference 64×18 layout. HTTP or TCP, scoped to the selected node and paged at up to 12 services per screen on the reference layout.
- Storage — filesystem capacity plus optional SMART disk health and detail.
- Containers — Podman/Docker summary, resource hotspots and container detail for local or remote nodes.
- Diagnostics — runtime display, touch, engine, services, config and sleep state, plus the active node, transport, connectivity and container access mode.
- Resilient remote state — per-node connectivity (
UP,DOWNor--), explicitSTALEsnapshots after transport failures and a shared SSH circuit breaker that suppresses redundant retries while a node is unavailable. - Touch wake guard — the first touch wakes a blanked display without activating the control underneath it; automatic collection is paused while the physical display is blanked and resumes with one fresh active-node refresh.
The UI automatically switches to the dedicated compact-touch layout on small
terminals such as the reference 64×18 console.
Remote SBCs do not need a TouchRack agent. HOST, STORAGE and CONTAINERS use
portable Linux interfaces and fixed read-only commands over the same hardened
OpenSSH transport used by node-scoped service providers. Optional narrowly
privileged helpers extend this model to SMART data and root-owned containers
without granting general sudo, Docker-group membership or arbitrary engine
access.
TouchRack itself requires:
- Linux with virtual consoles (
/dev/ttyN) - Python 3.11+
- a Unicode-capable Linux console font
openvtandsetfontsettermfor display blank/wakepython3-venv(or equivalent Pythonvenvsupport) for the supplied installer- systemd for the supplied boot-time deployment
- a touchscreen exposed through
/dev/input/event*only when touch is enabled
Optional features add their own external tools:
smartctlfor SMART health in STORAGE- Podman or Docker for CONTAINERS
systemctlforsystemdentries in SERVICES
On Ubuntu 24.04, the reference host dependencies are:
sudo apt install python3 python3-venv kbd util-linux console-setup-linuxOptional monitoring features:
sudo apt install smartmontools
sudo apt install podman # or install Docker / docker.ioRemote nodes require the system OpenSSH client on the TouchRack host. Remote
SMART monitoring additionally uses the narrowly privileged helper documented
in docs/REMOTE_SMART.md.
The reference Ubuntu setup uses:
/dev/tty1
/usr/share/consolefonts/Uni3-Terminus32x16.psf.gz
1024×600 → 64×18 cells
Python runtime dependencies (textual, psutil, evdev on Linux and
PyYAML) are declared in pyproject.toml and installed into the TouchRack
virtual environment.
See docs/DEPENDENCIES.md for the complete dependency
matrix, including build/development and optional feature dependencies.
No X11, Wayland or browser is required.
Clone the repository and create a virtual environment:
python3 -m venv .venv
.venv/bin/pip install -e .Create local configuration from the examples:
cp config.example.yaml config.yaml
cp services.example.yaml services.yamlFor a complete local + remote SBC setup instead:
cp config.multi-node.example.yaml config.yaml
cp services.multi-node.example.yaml services.yamlFor a quick manual TTY test:
sudo systemctl stop getty@tty1.service
sudo openvt -c 1 -f -s -- \
env LANG=C.UTF-8 LC_ALL=C.UTF-8 PYTHONUTF8=1 TERM=linux \
"$(pwd)/.venv/bin/homelab-console"For normal boot-time use, deploy the supplied systemd unit:
sudo ./scripts/install-systemd.sh
sudo systemctl restart homelab-touch-console.serviceThe installer builds a non-editable, root-owned runtime in
/opt/touchrack, keeps runtime configuration in /etc/touchrack, and
points systemd at those paths. The source checkout remains a development
tree and is never executed directly by the root service.
On the first deployment, an existing local config.yaml and
services.yaml are migrated into /etc/touchrack. Later deployments do
not overwrite those files. scripts/uninstall-systemd.sh removes the
runtime and unit but deliberately preserves /etc/touchrack.
To enable the service at boot as well:
sudo systemctl enable --now homelab-touch-console.serviceCheck it with:
systemctl status homelab-touch-console.service
journalctl -u homelab-touch-console.service -bThe versioned service intentionally launches through:
openvt -c 1 -f -s -w
This is the startup mode validated on the reference physical console.
config.yaml is optional. Lookup order is:
HOMELAB_CONFIG./config.yaml~/.config/homelab-console/config.yaml- built-in defaults
Example:
display:
tty: /dev/tty1
sleep: 1m
refresh:
seconds: 30
touch:
enabled: true
device: auto
services:
file: services.yamlSupported sleep values are 1m, 5m, 15m and on.
touch.device: auto discovers the touchscreen automatically; an explicit
/dev/input/eventX path may also be used.
Remote Linux SBCs are configured as nodes with an SSH transport. TouchRack uses a dedicated unprivileged account, a per-node identity, strict pinned host keys, fixed timeouts and the system OpenSSH client:
nodes:
- id: touchrack-host
title: TouchRack Host
transport:
type: local
- id: remote-sbc
title: Remote SBC
transport:
type: ssh
host: 192.168.50.20
user: touchrack-monitor
identity_file: /etc/touchrack/ssh/remote_sbc_ed25519
known_hosts_file: /etc/touchrack/ssh/known_hostsThe node selector is explicitly paginated for touch use on the 64×18 console:
three nodes are shown per page with PREV / NEXT controls and a page
indicator. Startup opens the page containing the active node rather than
assuming the whole fleet can fit on screen.
HOST, STORAGE, CONTAINERS and node-scoped SERVICES reuse the same node runtime
and transport. Remote HOST collection is agentless and reads portable Linux
interfaces such as /proc, /sys, df and ip. Remote SERVICES supports
both systemd and native CRUX /etc/rc.d/<service> status providers, while
CONTAINERS prefers Podman and falls back to Docker.
Connectivity is tracked independently per node and shown as UP, DOWN or
--. HOST, STORAGE and CONTAINERS retain their last successful snapshot after
a transport failure and mark it explicitly as STALE, preserving the original
collection time so its age continues to increase. A shared SSH circuit breaker
prevents each capability from repeatedly opening its own connection while a
node is unavailable and permits a single recovery probe after cooldown.
When TouchRack blanks the physical display, scheduled collection is paused as well: no periodic SSH sessions are opened, missed intervals are not replayed, and waking the console triggers one immediate refresh for the active node before the normal interval resumes.
See docs/REMOTE_NODES.md for the complete secure
provisioning procedure, capability matrix, CRUX examples and failure semantics.
Root-owned Podman or Docker workloads can be monitored through the opt-in,
fixed-command helper described in
docs/REMOTE_CONTAINERS.md, without granting
Docker-group membership or general container-engine access.
The two-node tested example lives in
config.multi-node.example.yaml.
SERVICES is a curated dashboard rather than another inventory. Define the
things that matter in services.yaml:
services:
- id: dashboard
title: Dashboard
provider: container
target: dashboard
metric: memory
pinned: true
priority: 10
- id: web
title: Web
provider: http
target: http://127.0.0.1:8080/health
expect: 200
timeout: 2
pinned: true
priority: 20Available providers:
| Provider | Check |
|---|---|
container |
Podman/Docker state and optional CPU/memory metric |
crux |
CRUX /etc/rc.d/<target> status over the node transport |
systemd |
systemctl is-active |
http |
HTTP status |
tcp |
TCP connection to host:port |
Every provider is normalized to the same UI states:
OK, IDLE, WARN, ERROR, UNKNOWN.
Provider failures do not bring down the dashboard; they are represented as service state instead.
flowchart LR
SYS["systemd + openvt<br/>Linux TTY"] --> APP["TouchRack<br/>Textual app"]
CFG["config.yaml<br/>services.yaml"] --> APP
TOUCH["Touchscreen<br/>evdev"] --> TR["TouchReader"]
TR --> APP
APP --> UI["Screens<br/>Host · Services · Storage · Containers · Diagnostics"]
APP --> NR["NodeRegistry"]
NR --> RUN["NodeRuntime"]
RUN --> TRANS["local / SSH<br/>CommandRunner"]
RUN --> HOST["HostProvider"]
RUN --> STORE["StorageProvider"]
RUN --> SMART["DiskHealthProvider"]
RUN --> CONT["ContainersProvider"]
RUN --> SM["ServicesManager"]
HOST --> LINUX["Linux / psutil"]
STORE --> LINUX
SMART --> SCT["smartctl (optional)"]
CONT --> ENGINE["Podman / Docker"]
SM --> CP["container"]
SM --> SP["systemd"]
SM --> CR["CRUX rc.d"]
SM --> HP["HTTP"]
SM --> TP["TCP"]
CP --> ENGINE
APP --> BLANK["Screen blank / wake guard"]
BLANK --> VC["setterm<br/>virtual console"]
The key boundary is simple: screens consume models/state; providers know how to obtain it. UI code does not need to know whether a service came from systemd, a container, HTTP or TCP.
The project is intentionally read-only by default. Monitoring failures
should degrade to UNKNOWN/ERROR, not crash the interface.
src/homelab_console/ # internal Python package
├── app.py application coordination and navigation
├── config.py application configuration
├── models.py host/container state models
├── providers/ host, storage, SMART and container providers
├── nodes/ node identity, registry, runtime and composition
├── transports/ local/SSH command runners and circuit breaker
├── screens/ Host, Services, Storage and Containers UI
├── services.py service registry/providers/normalization
├── touch.py direct evdev touchscreen input
├── screen_blank.py Linux VC sleep/wake helpers
├── single_instance.py single-instance lock
├── button_actions.py pure button-routing classification
├── refresh_policy.py pure refresh interval policy
├── runtime_diagnostics.py diagnostics formatting
└── console.tcss Textual stylesheet
Install development dependencies:
.venv/bin/pip install -e '.[dev]'Run the suite:
.venv/bin/pytestThe UI smoke tests use Textual's test driver at the physical target size (64×18) so stylesheet/parser regressions are caught before deployment.
CORE-LOGIC-SHA256.txt records the known-good hashes of the sensitive touch,
blanking and single-instance modules.
These are intentionally not versioned:
config.yaml
services.yaml
.venv/
Start from config.example.yaml and services.example.yaml instead.
