The sithbit-console admin TUI
sithbit-console is the terminal management console for a SithBit
deployment: a keyboard-driven UI that lets an operator inspect accounts,
mailboxes, and messages (with their on-chain delivery states), watch the
job queues, and requeue or discard dead-lettered jobs — without ever
touching the store or the chain state that the mail servers own.
Its defining design constraint: the console talks only to the account
API’s /v1/admin routes. Every read and every action round-trips
through the API, never the database directly. That keeps the operator
surface behind the same authentication and admin-wallet allowlist the API
already enforces, works identically against every storage backend (SQLite
or any of the cloud stores), and means the console can run from any
machine that can reach the API — it needs no store credentials at all.
The one deliberate exception is the read-only balances
pane, which reads public chain state through the
mail-grpc gateway and a Solana RPC node.
Prerequisites
- A running account API (standalone
account-api, or the one inside yoursithbitddeployment) reachable from the console’s machine. - Your wallet on the API’s admin allowlist. The console logs in with
a Solana wallet keypair; the API only serves
/v1/adminroutes to wallets listed in itsadmin_walletssetting. An emptyadmin_walletslist disables the admin surface entirely — every admin call (and therefore every console pane) replies 403. - The wallet’s keypair file on the console machine (a standard
Solana 64-byte JSON keypair; default
~/.config/solana/id.json). - Optionally, for the balances pane: a reachable mail-grpc gateway and a Solana JSON-RPC node. A console used only for the admin panes can ignore both.
Running it
cargo run -p mail-console --bin sithbit-console
With no configuration at all, the console targets the loopback dev stack:
the account API at http://127.0.0.1:8180, the mail-grpc gateway at
http://127.0.0.1:50051, and a local RPC node at http://127.0.0.1:8899,
signing in with ~/.config/solana/id.json.
On startup the console:
- loads its configuration (next section),
- loads the keypair and performs the wallet-challenge login
(
/v1/auth/nonce→ signature →/v1/auth/token→ JWT) — a failure here (API down, malformed keypair) exits with the error before any UI appears, - connects the balance client (the gateway channel dials lazily, so a down gateway does not block startup — only a syntactically bad endpoint URL does), and
- opens the terminal UI on the Accounts tab, loading the account list.
Note that the login succeeding does not yet prove the wallet is an
admin: the allowlist is checked per admin route, so a non-admin wallet
gets a working UI whose every pane reports a 403 error in the status bar.
Fix the API’s admin_wallets and press r.
Configuration
Config file sithbit_console.toml in the working directory (or the path
in SITHBIT_CONSOLE_CONFIG), env prefix SITHBIT_CONSOLE, resolved
through the standard layering (in-code default → TOML → .env →
.env.$APP_ENV → environment). Every entry defaults, so an empty or
absent file runs against the loopback dev stack:
# api_url = "http://127.0.0.1:8180"
# keypair_file = "~/.config/solana/id.json"
# gateway_endpoint = "http://127.0.0.1:50051"
# rpc_url = "http://127.0.0.1:8899"
The four keys are documented in the configuration reference.
The two tabs
The console has two top-level tabs, switched with Tab:
- Accounts — the wallet list, drilling down through mailboxes to messages, plus the balances pane.
- Queues — job-queue depths and the dead-letter table with its requeue/discard actions.
A status bar along the bottom shows the last result or error on the left
and the active tab’s key hints on the right. All data is fetched on
demand; r reloads the pane you are looking at.
Tutorial: inspecting accounts and mail
Accounts
The Accounts tab opens on the wallet list — every account the API knows,
one base58 wallet address per row. j/k (or the arrow keys) move the
selection; the status bar shows the total (N accounts).
Mailboxes
Press Enter on a wallet to list its mailboxes (INBOX, folders created
over IMAP, and so on). Unselectable hierarchy-only entries are marked
(noselect). Esc steps back to the wallet list.
Messages
Press Enter on a mailbox to open its message table:
| Column | Meaning |
|---|---|
uid | The message’s IMAP UID in this mailbox |
size | Stored size in bytes |
date | The message’s internal date (first 19 chars, YYYY-MM-DDTHH:MM:SS) |
chain | The delivery-pipeline chain state — local (dark gray) for a copy with no chain record |
cid | The IPFS CID of the sealed body, once pinned (empty until then) |
Rows are color-coded by chain state: terminal states draw attention, in-flight ones stay calm. This is the fastest way to answer “did that message actually make it on-chain, and what CID did it get?” for a specific user without querying the store by hand.
The balances pane
Press b on a wallet (in the wallet list, or anywhere deeper in that
wallet’s drill-down) to open its on-chain balances:
- Summary — native SOL (in SOL and lamports), the mailbox’s default stamp price in lamports, and its lifetime mail count.
- Senders — one row per other loaded wallet: the prepaid stamps it
holds toward this mailbox and the per-mail postage it owes
(
from/stamps/required (lamports)).
This is the console’s only direct-to-chain view: the summary and stamp
rows come from the mail-grpc gateway (GetMailbox, GetFrombox) and the
SOL balance from the JSON-RPC node — all read-only public chain state,
nothing signed. If neither endpoint is configured and reachable, the
pane reports an error while every admin pane keeps working.
Esc returns from any drill-down level toward the wallet list.
Tutorial: queues and dead letters
Switch to the Queues tab with Tab. The pane stacks two tables:
- Queues — one row per job queue
(
queue/depth/oldest), whereoldestis the age of the oldest waiting job in seconds. A depth the backend cannot report cheaply shows as?. - Dead letters — jobs that exhausted their retries
(
queue/type/attempts/died/reason), wheretypeis the payload’s job type tag.
Two actions work the dead-letter table, both requiring confirmation:
u— requeue the selected job (put it back on its queue for a fresh attempt, e.g. after fixing the outage that killed it).x— discard the selected job permanently.
Either key arms the action and the status bar asks
requeue <queue> job (<reason>)? y/n — press y to run it, any other
key to cancel. On cloud stores a listed dead job stays claimed for
about five minutes; requeue/discard echo the claim token back, so if the
claim has lapsed (or another operator acted first) the action fails
cleanly and a reload (r) shows the current truth.
Key reference
| Key | Context | Action |
|---|---|---|
j / ↓ | everywhere | Move selection down |
k / ↑ | everywhere | Move selection up |
Enter | Accounts tab | Drill down (wallet → mailboxes → messages) |
b | Accounts tab | Balances pane for the selected wallet |
Esc | Accounts tab | Step back up one level |
Tab | everywhere | Switch between the Accounts and Queues tabs |
r | everywhere | Reload the current pane |
u | Queues tab | Requeue the selected dead job (asks y/n) |
x | Queues tab | Discard the selected dead job (asks y/n) |
y | confirmation | Confirm the armed requeue/discard |
q | everywhere | Quit |
What the console deliberately does not do
- No store access. It links no storage backend; it cannot see rows
the API does not expose. (The
/v1/adminsurface is the contract — anything you can script against it, the console can show.) - No mail content. Message bodies are sealed to their recipients; the console shows metadata and chain state only.
- No chain writes. The balances pane is read-only; the console signs nothing but its own login challenge.
- No server management. Starting, stopping, and configuring the daemons stays with your process supervisor and config files.
Troubleshooting
| Symptom | Likely cause |
|---|---|
Exits immediately with logging in to <url> | Account API down or unreachable at api_url, or the keypair file is missing/malformed |
| Every pane shows a 403 error | The logged-in wallet is not in the API’s admin_wallets allowlist (or the list is empty, which disables the admin surface) |
| Balances pane errors, admin panes fine | gateway_endpoint / rpc_url not reachable — expected on an admin-only setup; the pane needs both |
| Requeue/discard fails after sitting on the pane | The cloud store’s ~5-minute dead-job claim lapsed; press r and act on the fresh listing |