Skip to content

Repository files navigation

Omix

Your Termix fleet in the Omarchy bar.

Omix is a read-mostly fleet status surface for Omarchy Quattro. Termix already has a web UI, a desktop app, a mobile app and a CLI — this is not a fifth client. Its job is to tell you when something needs attention, and get you to the right place in Termix in one click.

Quiet when everything is fine. That is the whole design.

Omix panel: overview, alerts and hosts


Bar states

Quiet, warning and critical on the bar

State Trigger Looks like
Quiet No active alerts, every host reachable, every tunnel healthy The glyph in your theme's foreground, no count, no dot
Warning Any warning-severity alert, or any tunnel down Amber glyph and dot, with a count
Critical Any critical alert, or any host down Red glyph and dot, with a count
Unknown Termix unreachable, a rejected key, or the first poll has not returned Dimmed glyph with a neutral dot, no count

Hover for a one-line summary: 2/3 hosts up, 2 alerts, 1 of 2 tunnels down. Left click toggles the panel, middle click forces a refresh. Right click is deliberately unbound.


Install

omarchy plugin add https://github.com/dreadnought-0/omix.git --enable --yes

Then set at least a Termix URL and a way to reach an API key, either through Setup → Plugins → Omix or by editing the widget's entry in ~/.config/omarchy/shell.json:

{ "id": "io.github.dreadnought-0.omix",
  "baseUrl": "https://termix.example.lan",
  "apiKeyCommand": "pass show homelab/termix-api-key" }

Omarchy stores every plugin's settings inline on its bar-layout entry, so there is no separate config file and no config: sub-object.

Requirements and dependencies

Omix bundles no binaries and installs nothing. It needs:

Requirement Why Where it comes from
Omarchy Quattro (tested on 4.0.1) Host shell Already present
curl Every HTTP request. The API key is passed as configuration on stdin, never as an argument. core/curl, present on every Omarchy install
omarchy-launch-browser Opens deep links back into Termix Ships with Omarchy
A reachable Termix instance and an API key The thing being monitored Yours

Optional: whatever command you use to fetch the key — pass, secret-tool, infisical, keyctl. Omix runs the command you configure and reads its stdout; it depends on none of them specifically.

Omix requires no privilege escalation, adds no systemd units, writes nothing outside its own state file, and never starts a second Quickshell process.

Removing it

omarchy plugin remove io.github.dreadnought-0.omix

That removes the checkout and takes the widget out of your bar layout. Two things it does not touch, so remove them yourself if you want a clean slate:

rm -rf ~/.local/state/omix          # cached fleet state, written 0600

and the API key, wherever you put it — Omix never copies it anywhere, so deleting the plugin leaves your pass entry or key file exactly as it was. If you used the plaintext apiKey setting, removing the widget from shell.json removes the key with it; rotate that key anyway, since it has been sitting in a config file.

To disable without removing:

omarchy plugin disable io.github.dreadnought-0.omix

Disabling a bar widget discards its settings. Omarchy disables a widget by taking it out of the bar layout, and the settings live on that layout entry, so re-enabling gives you a fresh widget with defaults — your Termix URL and key path are gone. Copy the entry out of shell.json first if you plan to come back. This is Omarchy's behaviour for every bar widget, not something Omix chooses.

Disabling stops the heartbeats immediately. Termix reaps any metrics viewer whose last heartbeat is over 120 seconds old, so collection stops on its own within two minutes; if you re-enable or restart the shell before then, Omix sweeps its own registrations at startup and it stops at once. See Live metrics for why it works that way rather than firing a final request on the way out.

Termix versions

Built and tested against Termix 2.7.1 (Termix-SSH/Termix, commit 42f8270, 2026-09-02).

The Termix API moves fast and the published reference lags the code. Omix was built by reading the route definitions in that release, not the docs, and it capability-probes every domain at startup rather than assuming an endpoint exists. A section whose endpoints are missing on your release is hidden with a reason, not left to throw errors on every poll. See CHANGELOG.md for the endpoints whose response shape had to be inferred rather than confirmed.


Getting an API key

Termix API keys are tmx_-prefixed, scoped to one user, and can be given an expiry date. Creating one is an admin-only operation in Termix 2.7.1.

  1. Sign in to Termix as an admin.
  2. Go to Settings → Admin → API Keys.
  3. Create a key. Give it a name that says what it is for (omix-bar-widget), scope it to the user whose hosts you want on the bar, and set an expiry date — a year is a reasonable ceiling for something living in a desktop.
  4. Copy the token. Termix shows it exactly once.
  5. Store it somewhere Omix can fetch it (below). Do not paste it into shell.json if you can avoid it.

Read this before you create one

A Termix API key is not a read-only monitoring token, and Termix has no read-only scope. The key you give Omix can do everything its user can do through the API — and Termix holds the SSH credentials for your entire fleet. The same key that reads "14/15 hosts up" can enroll hosts, create credentials, open sessions and share them.

Omix itself never calls any of those endpoints — see What Omix will not do — but the key does not know that. Treat it as a fleet-wide credential:

  • Give it an expiry, and rotate it.
  • Scope it to a user that only has the hosts you actually want on the bar.
  • Keep it out of shell.json.
  • If you turn on actions, that key now needs write access to do them, and a compromise of your desktop session is a compromise of your fleet. That is a real trade for a one-click container restart, and it is off by default for that reason.

If a read-only or monitoring-scoped key ever lands in Termix, this plugin should be reconfigured to demand it.


Where the key lives

Resolved in this order, first hit wins.

1. apiKeyCommand — recommended

A command printing the key on stdout. Run once at startup and on explicit refresh. The key never touches shell.json.

{ "apiKeyCommand": "pass show homelab/termix-api-key" }

Works with anything that prints a secret:

{ "apiKeyCommand": "secret-tool lookup service termix" }
{ "apiKeyCommand": "infisical secrets get TERMIX_API_KEY --plain" }
{ "apiKeyCommand": "keyctl print %user:termix" }

A worked pass setup:

pass insert homelab/termix-api-key      # paste the tmx_… token, then Ctrl-D
pass show homelab/termix-api-key        # confirm it prints one line

Then set apiKeyCommand to pass show homelab/termix-api-key. If your password store needs an unlocked GPG agent, unlock it before the shell starts, or Omix reports the command's failure in the panel and retries on refresh.

2. apiKeyFile

A file containing only the key.

{ "apiKeyFile": "~/.config/omarchy/secrets/termix-api-key" }

Omix refuses to read it unless its permissions are 0600 or tighter, and tells you the exact chmod to run. A key file your group can read is a fleet-wide credential sitting in the open.

install -m 600 /dev/null ~/.config/omarchy/secrets/termix-api-key
printf '%s' 'tmx_…' > ~/.config/omarchy/secrets/termix-api-key

3. apiKey — plaintext, last resort

{ "apiKey": "tmx_…" }

This stores a fleet-wide credential in plaintext in ~/.config/omarchy/shell.json. Anything that can read your config can read your fleet — every other plugin in the shell, any backup of your dotfiles, any config repo you push. Omix supports it, warns once in the panel while it is in use, and this README says it plainly: prefer the command.

How the key is handled

  • It is passed to curl as configuration on stdin, never as a command-line argument. Anything in argv is readable from /proc by every local user for as long as the process runs, and a widget polling every 30 seconds would publish your fleet key on a loop.
  • It is never logged, never rendered, never put in a tooltip, and never written to the state cache. Every string that can reach the UI — including curl's own error text — is passed through a redactor first.
  • 401 responses are told apart: an expired key, a revoked key and a locked data key each produce a different, actionable message.

Configuration

Every key below is set on the widget's entry in shell.json, or through Setup → Plugins → Omix.

Key Default What it does
baseUrl "" Termix API base URL. The only host Omix ever contacts.
apiKeyCommand "" Command printing the key. Preferred.
apiKeyFile "" File holding the key. Must be 0600 or tighter.
apiKey "" Plaintext key. Warned about, in use.
verifyTls true Turn off only if you cannot supply a CA bundle.
caBundle "" PEM bundle for a private CA. The right fix for self-signed.
webUrl baseUrl Where deep links open. Set it when the API and UI hosts differ.
openWith browser browser or desktop. See Deep links.
pollIntervalSeconds 30 Background cadence while the panel is closed.
panelPollIntervalSeconds 10 Cadence while the panel is open.
barContent worst worst, alerts, hosts, combined, or glyph.
enableLiveMetrics true See Live metrics.
allowActions false See Actions.
ignoreHosts "" Comma-separated host names to leave out of everything.
onlyFleets "" Comma-separated fleet names to scope the whole plugin to.
certExpiryWarnDays 21 Certificates expiring sooner are called out.
cpuWarnPercent 90 Tints a CPU bar. Does not create alerts.
memWarnPercent 90 Tints a memory bar.
diskWarnPercent 85 Tints a disk bar.
sectionAlertssectionActivity true (activity: false) Turn whole panel sections off.

sections

A section turned off issues zero requests, rather than being fetched and hidden. Turning off Docker means Omix never opens an SSH session to list containers; turning off Certificates means it never reads a host's certbot inventory.

Two deliberate exceptions: alerts and tunnel statuses are polled whether or not their sections are on, because they are two of the bar's three signals. Turning off the Alerts section hides a panel view; it must not blind the bar, or the state table at the top of this README would quietly stop being true. Both are single fleet-wide requests, so this costs nothing extra.

sectionActivity is off by default: the audit log is admin-only on most instances, and it is the noisiest thing Termix will hand you.

onlyFleets

Scopes every count and list to the named fleets. Useful when your Termix instance covers far more hosts than you want represented by one bar glyph.

Fleet membership is only known once the Fleets section has loaded. Until then the filter is not applied — applying half of it would silently hide hosts and make the bar read as healthier than it is.

Live metrics, and why they are opt-out

Termix's Host Metrics domain is viewer-driven: the server collects metrics for a host only while something is registered as watching it. That makes a careless bar widget a permanent load on every machine in your fleet.

With enableLiveMetrics: true (default), Omix:

  • registers as a viewer only while the panel is open on the Hosts section, and only for the host rows actually on screen — never the whole fleet;
  • debounces registration behind scrolling, so dragging through a long list does not start collection on forty machines;
  • sends heartbeats while those rows stay visible;
  • unregisters on every exit path it can complete one on: panel close, section change, scrolling a row away, and session idle;
  • writes its registrations to a journal on disk before it makes them, so a shell that is killed outright (kill -9, a crash, a power cut) leaves a record behind — and the next startup sweeps the sessions it orphaned.

That last point is the one that matters, and it is why the journal exists: a destructor cannot run if the process is killed, so correctness cannot depend on one.

There is one path Omix cannot close cleanly, and it is worth being plain about. When the plugin is disabled or removed, its subprocesses are destroyed with it — a final unregister request is killed before curl can finish. Two things cover that: heartbeats stop instantly, and Termix reaps any viewer that has not heartbeated for 120 seconds. So collection stops within two minutes rather than immediately, and immediately if the shell comes back and sweeps the journal. Handing the request to a detached process would close the gap, but only by putting a fleet-wide key into a command line or a file on the way out, which is a bad trade for ninety seconds.

With enableLiveMetrics: false, Omix never registers a viewer at all and runs on host statuses, alerts and tunnels alone. This is a fully supported mode, not a degraded afterthought: you lose per-host CPU/memory/disk and the sparklines, and you keep every other signal including the entire bar state model. If you would rather your fleet never collected metrics on a bar widget's behalf, this is the setting.


The panel

Twelve sections. Every screenshot below is the real widget against a fleet of ten hosts — one down, two reachable-but-sessionless, a failed unit, an expired certificate and a failing backup job.

Overview and alerts

Overview Alerts
Overview Alerts
Counts across the fleet, each linking to its section. Satisfiable from three requests whatever the fleet size. A dash means that count costs an SSH connection per host and has not been asked for. Active firings from Termix's own alert engine, severity-ranked, with the rail colour-coded. Dismiss inline — allowed even with actions off, because it changes your alert list and touches no machine.

Hosts and tunnels

Hosts Tunnels
Hosts Tunnels
Problems first, then unknown, then healthy. Expanding a row shows live CPU, memory and disk with a sparkline, and the three deep links into Termix. Note <b>evil</b>…: a host name containing markup renders as inert text. Every tunnel with its state, failure reason and retry count. Presets listed separately. This is the one Termix domain that is genuinely cheap fleet-wide.

Docker, Proxmox and services

Docker Proxmox Services
Docker Proxmox Services
Containers per host. Termix's Docker API is session-based, so Omix connects, lists, and disconnects rather than holding a session open while you read. Guests and cached node stats from Termix's own Proxmox sync. Hides itself entirely when a dedicated Proxmox plugin is installed. Failed units and enabled-but-stopped units, by exception. A full unit list is hundreds of rows that answer no question. Processes load separately, on request.

Network, certificates and automations

Network Certificates Automations
Network Certificates Automations
Tailscale state and IPs, WireGuard interfaces and peers, per host. Certbot and acme.sh certificates, soonest expiry first. Expired is red, inside your window is amber. Termix has no alerting for this. Trigger type, last run and last result, failures first — the way a CI widget would show them.

Fleets and activity

Fleets Activity
Fleets Activity
Groupings and their members. onlyFleets scopes the entire plugin to the ones you name. Recent audit entries, failures in red. Off by default: the audit log is admin-only on most instances.

When something is wrong

Termix unreachable

Termix down, a rejected key, a TLS failure and an expired key each produce a different, actionable message. The bar goes to Unknown — never to all-clear — and polling backs off to a five-minute ceiling rather than hammering a machine that is already down. Opening the panel retries at once.


What is in the panel

Overview — hosts up over total, active alerts by severity, tunnel health, and the per-host counts once their sections have been opened. Every tile links to its section. Satisfiable from three requests regardless of fleet size.

Alerts — active firings with severity, host, message and when they fired. Dismiss inline. Omix does no threshold evaluation of its own; Termix's alert engine already did it, with your durations, your cooldowns and your notification channels. A second opinion here would just disagree with the alerts you configured.

Hosts — every host with reachability, tags, and live CPU/memory/disk with a sparkline when metrics are on. Text filter and a problems-only filter. Problems sort to the top.

Tunnels — every SSH tunnel with state, failure reason and retry count. Presets listed separately.

Docker — containers grouped by host, on demand.

Proxmox — guests and cached node stats, read from Termix's own Proxmox sync. Omix never talks to Proxmox directly. This section hides itself when a dedicated Proxmox plugin is installed (it probes omarchy plugin list --json for proxmox/omaprox ids), because those cover PBS jobs, per-storage detail, task history and quorum state that Termix does not expose — and two shallow copies of the same guest list help nobody.

Services — failed units, and enabled units that are not running, by exception. A full unit list is hundreds of rows that answer no question. Top processes load separately, per host, on request.

Network — Tailscale state and IPs, WireGuard interfaces and peers.

Certificates — certbot and acme.sh certificates, soonest expiry first, with your warning threshold. Quietly one of the most useful views here: in a homelab behind a reverse proxy, the thing that takes everything down at once is a certificate nobody was watching. Termix has no alerting for it.

Automations — trigger type, last run and last result, failures first.

Fleets — groupings and their members.

Activity — recent audit entries. Off by default.

Deep links

The highest-value thing here. Omix does not need to be a terminal; it needs to get you to one in a click. Each host row opens Termix's terminal, metrics or file manager view for that host, and the panel header opens Termix itself.

openWith: "browser" opens ?view=…&hostId=… in your browser and lands on the exact host.

openWith: "desktop" launches the Termix desktop app and stops there. It cannot target a host, for two reasons that are Termix's rather than ours: Termix registers no URL scheme, and the desktop app talks to its own embedded local backend, whose host ids belong to a different database than your server's. Browser mode is the default because it is the one that actually works.


Actions

All actions sit behind allowActions: true, which is off by default. With it off, no action control is reachable anywhere in the UI — not disabled, not greyed out: absent, so there is nothing to mis-click.

allowActions: false (default) allowActions: true
No action controls Confirmation naming the target
The same section with actions off. There is no control to click. Every action confirms first, and the confirmation names the exact targetRestart caddy?, not Restart?. A bar panel is a small surface and the row you meant is one pixel from the row you did not.

With it on, Omix can:

  • restart a systemd service on a host
  • restart a Docker container
  • bring an SSH tunnel up or down
  • bring a WireGuard interface up or down
  • connect or disconnect Tailscale on a host
  • trigger an automation

Every one of them shows a confirmation naming the exact target (Restart nginx.service?) before it fires, and reports its result inline at the top of the panel. Nothing is bound to a bare click.

Dismissing an alert is allowed even with actions off: it changes your own alert list and touches no machine.

Actions are deliberately not exposed over IPC. A confirmation needs someone to confirm to.

What Omix will not do

Not at any setting, not behind any flag. Some of these are dangerous from a bar click; the rest are Termix's job, and the deep link takes you there.

  • Embed or proxy a terminal, RDP, VNC, Telnet or serial session
  • File manager operations of any kind
  • Read, create or modify credentials, or touch the Vault integration
  • Create or modify SSH hosts, enroll hosts, or handle Termix ID keys
  • RBAC changes, host sharing, user management, or API key management
  • Create session share links
  • Send signals to processes, including kill
  • Anything involving Termix's AI assistant endpoints
  • Guacamole token generation

Privacy

Omix contacts the Termix instance you configure and nothing else. No telemetry, no analytics, no update checks, no third-party requests of any kind. The only other processes it runs are your apiKeyCommand, curl, omarchy-launch-browser for deep links, and omarchy plugin list --json once at startup to check whether a dedicated Proxmox plugin is installed.

Omix does not use sudo or pkexec. It never writes outside its own state file, and never starts a second Quickshell process.

The state cache lives at ~/.local/state/omix/state.json, written 0600. It holds rendered state — host names, counts, the viewer journal — so that reopening the panel is instant. It never holds a credential.


Troubleshooting

TLS verification failed. Omix says so and names the fix. Point caBundle at your CA's PEM file: "caBundle": "~/.config/omarchy/secrets/homelab-ca.pem". Setting verifyTls: false also works and is a bad idea — your API key rides every request, and without verification you cannot tell your Termix instance from anything else answering that address.

"Termix API key has expired" / "Termix rejected the API key". These are different on purpose. The first means the key hit its expiry date: make a new one. The second means Termix does not recognise it: it was deleted, deactivated, or mistyped.

"API key file is readable by other users". Run the chmod 600 the message gives you. Omix will not read a key file your group or the world can read.

A section is missing. Either you turned it off, or its endpoints are not present on your Termix release and the capability probe hid it. Omix prefers hiding a section to spamming errors on every poll. Turn on the Overview and check the section list.

Live metrics unavailable, TOTP mentioned. Your instance gates metrics behind a TOTP challenge. A bar widget has nowhere to prompt for a code and must not try, so Omix stops registering viewers and says so. Host status, alerts and tunnels are unaffected. Set enableLiveMetrics: false to stop it asking.

The API and UI are on different hostnames (reverse proxy, split DNS). Set webUrl to the UI hostname; baseUrl stays the API one. Deep links use webUrl.

Termix unreachable. The bar goes to Unknown — never to "all clear" — the panel shows why, and polling backs off exponentially to a five-minute ceiling rather than hammering a machine that is down. Opening the panel retries at once.

Nothing happens after editing a .js file under the plugin. Omarchy hot-reloads .qml but not .js. Run omarchy restart shell.


Development

node tests/model.test.js     # pure JS: parsing, redaction, bar state, viewer lifecycle
./tests/integration.sh       # the real Service.qml against a stub Termix, headless
./tests/crash-recovery.sh    # kill -9 mid-session, assert the next start cleans up
omarchy plugin validate .
qmllint -I <import-root> *.qml sections/*.qml components/*.qml

The integration test runs the actual Service.qml under Quickshell against a stub Termix (tests/stub-termix.js) without touching your running bar. It asserts the things unit tests cannot reach: that the key never appears in a process argument, that a 404 endpoint hides its section, that every per-host section actually populates, and — checked against the stub server's own view rather than the plugin's — that no metrics viewer registration survives the panel closing.

crash-recovery.sh goes further and kills the harness with SIGKILL partway through, then asserts that a second run releases what the first one orphaned. It resolves its key through a command rather than inline on purpose: that is the asynchronous path, the cache file loads before the key does, and an implementation that spends its one sweep before the key arrives silently never cleans up. That bug existed and this test is why it does not.

Everything under api/ and model/ is plain JavaScript with no QML imports, so it can be exercised in node directly.


Relationship to a dedicated Proxmox plugin

Termix aggregates Proxmox behind its own API, so Omix overlaps with a dedicated Proxmox plugin — but it does not replace one. Termix's Proxmox coverage is guest discovery plus cached node stats: no PBS backup job results, no per-storage detail, no cluster task history, no quorum state. Run both if you want depth on Proxmox and breadth across the fleet; Omix stands its own Proxmox section down automatically when it finds one installed.

License

MIT. See LICENSE.

Termix is Apache 2.0, by Luke Gustafson. Omix is an independent third-party plugin and is not affiliated with or endorsed by the Termix project.

About

Your Termix fleet in the Omarchy bar: alerts, host health, tunnels, Docker, Proxmox and certificates, with one-click jump back into Termix.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages