Skip to main content

Terminal Console (utconsole)

A read-only terminal view of a running UltraTorrent install: one static binary, no runtime, no configuration beyond a server URL and an account.

The Overview view: system, transfers, storage, work and attention

It observes and never manages. Every request it makes is a GET against /api/operations, authenticated as an ordinary account. That is not a promise the binary makes about itself — the server has no mutating route on that surface and refuses everything else regardless of what a client asks for. Read-only-ness comes from the API and the account's role; the console being incapable of writing is defence in depth, not the mechanism.

Why a terminal client

The web app answers "what is happening" beautifully and needs a browser, a session and a screen. An operator on an SSH connection to a NAS at 2am has a terminal. The console is for that: the same facts, the same permissions, over the same API, rendered where the operator already is.

Install

The binary is built from clients/console/ and depends on nothing at runtime:

cd clients/console
./build.sh # writes dist/ for linux, darwin, windows
scp dist/utconsole-linux-amd64 host:/usr/local/bin/utconsole
ssh host chmod 755 /usr/local/bin/utconsole

CGO_ENABLED=0 throughout, which is what lets one binary run on both a current Ubuntu and a NAS whose glibc is years older. A dynamically linked build fails on the older one with a link error that says nothing about the real cause.

First run

utconsole login --server https://your-install # once; stores a rotating token
utconsole # the console
Point at the application root, not the backend port

The backend container typically publishes no port; the frontend proxies /api and /ws/ through to it. Use the URL you open in a browser. Pointing at :4000 directly works only where that port is actually published.

CommandWhat it does
utconsoleThe interactive console
utconsole login --server URL [--user NAME] [--totp CODE]Authenticate; stores a rotating refresh token
utconsole logoutForget the stored session
utconsole snapshot [--domains a,b]Print one reading as JSON and exit
utconsole versionBuild and contract version

snapshot exists so the console is useful in a pipeline and in a bug report, not only on a screen — and because it is the smallest thing that proves a deployment works end to end.

Keys

KeyAction
tab / 19Switch view
rRefresh now
pPause polling entirely
fCycle the stream's category filter
qQuit

What an account needs

The console.view permission, which grants access to the client and nothing else. Every panel is still gated by that domain's own view permission, so a console user sees exactly what the same account sees in the web app — no more. READ_ONLY, USER and POWER_USER hold it out of the box; ADMINISTRATOR inherits it.

There is deliberately no console.admin. A grant that bypassed domain permissions would make the console the one client where RBAC does not apply, which is the opposite of the point.

An account holding console.view and no domain permissions is refused at startup with a message saying so, rather than opening onto nine empty views. A panel the account may not read says so in its own frame, dimmed rather than coloured like a fault — a permission boundary is not an incident, and colouring it like one teaches an operator to ignore the colour that means something is wrong.

The views

Overview

The host on the left, the work on the right, so "is the machine sick or is the workload sick" is answered by looking at one side.

Overview

Load is shown per core, because a raw load average means nothing without knowing how many cores it is spread across — 6.0 is an emergency on two cores and an idle afternoon on sixty-four.

Torrents

Torrents: needs attention, active transfers, and the scheduler queue

Needs attention comes first and holds anything errored or stalled — a torrent downloading with no peers and no throughput. Active is capped by the server, and the console says so rather than letting a list that stops at 25 read as "that is all of them".

Transfer figures carry an observed age: the server reads them from what its engine poller last saw rather than asking the engines again on your behalf, so the data is deliberately up to two seconds old and says so.

Media

Media: library counts, live playback and the intake pipeline

Jobs

Jobs: platform job counts, recent jobs and automation runs

Acquisition

Acquisition: RSS feeds and recent release decisions

A feed's state is staleness against its own refresh interval, not an error column. RSS poll failures are logged and never persisted, so an error field could only ever be empty — and a column that is structurally always empty reads as "no feed has ever failed", which is worse than not offering it. A feed is overdue after twice its interval; once would flag every poll that lands a moment late.

Release results use the vocabulary the platform can actually derive: downloaded, skipped_duplicate, matched, no_match. matched is kept distinct on purpose — it means a rule wanted a release and it was not taken, which is the state worth an operator's attention and the one a plain "rejected" would bury.

Infrastructure

Infrastructure: engines, indexers and providers

Health is carried by a glyph as well as a colour ( healthy, degraded, down, never reached). Colour alone excludes anyone with a colour vision deficiency and disappears entirely through a pipe.

Activity

Activity: the recent audit feed and notification delivery

A line marked (N events) is a collapsed burst. The console shows the count and cannot expand it — the snapshot carries a number, not the constituents — and pretending otherwise would be a lie about what it holds.

Alerts

Alerts: the attention list, framed in its worst severity

Alerts are a projection, not an entity

They are computed from health, job, intake, storage and provider state each time a snapshot is built. They have no identity that survives a restart, they cannot be acknowledged, and there is no dismiss key — the way to make one go away is to fix what it reports. A dismiss key would promise something the server cannot honour.

The pane is framed in the worst severity it holds, so a critical alert is visible before a word of it has been read.

Stream

Stream: the live event feed

A live narrative over a websocket, not polling. It is not history: it holds the last 200 events that arrived while this console was open, it does not backfill, and the view says so every time it renders. The record of what happened is the audit log.

f cycles the filter through whichever categories are actually in the buffer, rather than a fixed list of every category the platform can emit.

How it treats the server

A console is watched by people and points at a machine that may be having a bad day, so it is deliberately cheap:

  • Each view requests only the domains it displays, never all sixteen.
  • The refresh interval is clamped up to the floor the server advertises, so a misconfigured console cannot become load.
  • p stops polling entirely rather than freezing a copy, so a paused console costs the server nothing at all.
  • A failed refresh leaves the last good reading on screen with the failure and its age in the status bar. A console that quits when the server hiccups is useless exactly when it is needed.
  • Nothing is measured locally. No CPU sampling, no disk probing, no direct database, Redis, engine, media-server or filesystem access.

Configuration

~/.config/utconsole/config.json, mode 0600, holding the server URL, display preferences and a rotating refresh token. No password is ever stored. Override the location with UTCONSOLE_CONFIG.

{
"serverUrl": "https://your-install",
"refreshToken": "…",
"username": "operator",
"refreshSeconds": 5
}

The token lives in a file rather than an OS keyring because these run on headless servers where no keyring daemon exists, and a keyring that silently falls back to a file is worse than a file that says so. The console warns once if the file is group- or world-readable but does not refuse to start.

Colour and terminals

The palette is ANSI-256 rather than truecolor, because this runs over SSH into whatever terminal an operator happens to have, and a theme that assumes 24-bit colour renders as mud on a basic one. On a Linux virtual console the same screen renders in the 16 ANSI colours.

TERM must be set

With no TERM at all the rendering library concludes it is not talking to a colour terminal and renders monochrome. This bites when launching from a context that does not inherit an environment:

openvt -s -- /usr/local/bin/utconsole # wrong: no TERM, renders flat
openvt -s -- env TERM=linux /usr/local/bin/utconsole # right

Compatibility

The console checks the operations contract version at startup:

Server contractResult
Same majorCompatible
Newer minorCompatible; fields this build does not know are ignored
Different majorRefused, naming both versions

Refusing beats rendering nonsense from a shape the client is guessing at.

Troubleshooting

SymptomCause
Not signed in, or the stored session expiredNo stored token, or it was rotated elsewhere. Run utconsole login again.
This account may not use the consoleThe account lacks console.view.
Panels say "Your account may not read this"Expected: the account lacks that domain's view permission.
Everything renders monochromeTERM is unset — see above.
incompatible operations contractThe server speaks a different contract major; upgrade whichever half is older.
Stream shows ✕ refusedThe identity was rejected, not the network. Check the account still exists and holds its permissions.
Stream shows ✕ disconnectedThe socket dropped. It reconnects on its own with backoff.
console.view missing after an upgradeThe permission is created at boot by the module-permission sync; check the backend log for Added 1 permission(s): console.view.

Further reading