7. The operator console and the command line
Every page, label and button below is as the console renders it on monetize main c9b473a5 (crates/monetize-ui-web/src/pages/*.rs, routes in app.rs:106-130), with the backend call behind it (crates/monetize-ui-server/src/serve.rs:26-203) and the gRPC service that answers. W = crates/monetize-ui-web/src, S = crates/monetize-ui-server/src.
7.1 What the console is
A client-side Leptos page served by the ONE box's own read-only backend (monetize-ui-server, the same shape as gunnar-ui-server), which talks to monetize over loopback. There are no roles: an operator is anyone signed in with a passkey (or the console is open on loopback with neither passkeys nor a token; S/auth.rs:8-13). Every write (every non-GET under /api/, the invites, a passkey registration) needs a fresh touch of the key, the step-up: the browser answers a 403 stepup with a touch and retries once (S/auth.rs:63-67; W/api.rs:50-85).
The six rules of the surface (the code names them by number in its comments):
- One number per screen is big. Everything else is a row.
- Never a wall of text. A paragraph lives behind a badge you tap.
- Plain words on the surface, jargon underneath. "wall", not
Enforced; a test forbidsmonetize.toml,the poller,MONETIZE_UI_on a page (W/pages/mod.rs:94-138). - A trend is one line:
▲ 12 GiB/wk · Full in ~9 weeks, with its uncertainty (~). - Sort by who needs attention, never alphabetically.
- Outcomes, not steps.
Sold bob 90 GiB · 1 980.00 SEKis the row; the ledger steps live behind a disclosure.
On every page: the top bar (W/app.rs:90-104) with the brand "monetize" (to /), the estate picker (Pick an estate, grouped by product, from GET /api/estates), the links Overview, Accounts, Guardian, Test buy, Sites, Iron, and the "You ▾" menu (the user name or "unlocked console", a "Passkeys" link, a "Log out" button). The CODE RED banner reads GET /api/code-red every 10 s and shows each line with its "Next: …" and an "Ack" button (POST /api/code-red/{key}/ack → EstateOrder.AckCodeRed). Estate pages hold one live feed (GET /api/events, an Events.Watch stream; a 5 s poll when the stream is gone) and show "live", "connecting …" or "feed ended".
7.2 The pages
Passkeys, /login (W/pages/login.rs)
| element | what it does |
| panel "Sign in", button Sign in with a passkey | GET /auth/passkey/login/start → WebAuthn → POST /auth/passkey/login/finish |
panel "Enrol the first operator" / "Add another key" / "Enrol a passkey": fields Admin token (first enrol only: the break-glass console_admin_token), Device label (optional); buttons Enrol this key / Add this key, Cancel |
GET /auth/passkey/register/start → POST /auth/passkey/register/finish |
| panel "Operators" (signed in): hint "Every operator can do everything here."; field Name; select The link works for 1 hour / 1 day / 7 days; button Invite an operator; list "Open invites" with Revoke | POST /auth/invites, POST /auth/invites/withdraw; an invite lives 24 h by default, 7 days at most |
| panels "Unlocked console", "Passkeys are off" | shown when the console is open, or MONETIZE_UI_WEBAUTHN_RP_ID is unset |
Join, /invite (W/pages/invite.rs)
"Join this console · An operator invited you." Field Device label (optional), button Register my passkey (POST /auth/invite/start {secret, label} → WebAuthn → POST /auth/passkey/register/finish).
Overview, / (W/pages/overview.rs)
"Overview · Every estate. Pick one to work in it." (GET /api/overview → Estates.Overview; GET /api/estates/{e}/summary per row.)
| element | what it shows or does |
| table "Estates": Map (thumbnail), Estate (link to its map; products, host), Machines, Customers, Revenue/mo (net of VAT, last 30 days; test money in brackets), Stripe (live/test), Health | health words: "serving", "N paid at the vendor need you", " down", " not asked yet", not installed, unreached, unfed |
| ended estates | behind a quiet <details> line |
| the last row, "New estate": fields Name (the declared sites), Product, Ready mail to; button Order | POST /api/estates/new → Estates.NewEstate (the operator bearer); the answer carries monetize's words and the pay link |
| section "Live revenue, last 30 days" | the big number |
The estate shell, /e/<estate>/… (W/pages/shell.rs)
Sidebar: the estate name and its serving badge ("checking …", "not responding", "monetize · · N paid at the vendor need you"); the menu Map, Customers, Money (with Tax), Offer, Events, Settings.
Map, /e/<e>/map (W/pages/shell.rs:117-178)
" · Map · Front, main and twin, and how each is doing now."
| element | what |
| panel "Board" | the estate drawn (GET /api/estates/{e}/map → EstateAdmin.Draw, the shared estate-svg); empty states: no machines, unreached/unfed, "The map did not load" |
product lines (W/pages/products.rs:35-90) |
per product: the reachability dot (what the product last said to the poller, never a call during the page load, crates/monetize-server/src/health.rs), the id, tenants, the fleet figure and bar, the rate, the cloud; links to Customers |
| panel "Machines" | role, host, who |
per product, "Machines" (W/pages/tenants.rs:236-322) |
the fleet number, the oversold and growth badges, the cloud panel (owned rows: id, size, where, → tenant, ref; badges "play iron", "this box"; "Needs you · N" tasks) |
Customers, /e/<e>/customers[?product=] (W/pages/tenants.rs)
" · Customers". Panel "Tenants", sort: closest to cap (id, figure, bar, word, a state badge); panel "Recently bought" with the link all activity →; panels "Silent" and "Friction". Empty states: "No products", "No tenants yet", "Nothing bought yet". (GET /api/products, GET /api/products/{p}/tenants?estate=.)
Customer, /e/<e>/customer?product=&tenant= (W/pages/tenant.rs)
| element | what it does |
| the big number and bar; panel "Using" (every meter); panel "Activity" (day strips, "■ refused"); panel "Backed by" (bought / on hand, Order, Provider id); link History → | GET /api/tenants/{p}/{t} (Usage.Read, Entitlement.Get, Ledger.Query, Entitlement.Held, Entitlement.PublicKey), …/activity (Usage.Activity) |
<details> "Entitlement" |
State, Order, Paid until, Source, Caps; "Signature: verified against monetize's key" or "does not verify"; a pending-push warning |
| button How much? (operator) opens panel "Buy" (one button per bundle, " · ") and panel "How much?" (a number per orderable meter, Country, button Quote: Sells, Price, Cost, Can serve; fields Vendor, Payer hint (optional); button Confirm: take the money or Confirm: place the order (free)) | GET /api/bundles → Purchase.Bundles; GET /api/quote → Purchase.Quote; POST /api/purchase → Purchase.Start |
| panel "Operator action" (hint "Gives room without payment: for trials, refunds and support."): fields Label (optional), State free/paid/grace/suspended/retention, Paid until (UTC); button Set entitlement…, then Confirm: set entitlement / Cancel | POST /api/entitlement/{p}/{t} → Entitlement.Set (a signed fact with source operator) |
Money, /e/<e>/money[?product=&tenant=] (W/pages/ledger.rs, money.rs)
" · Money". Every money button is a MoneyAction: the first press arms it and asks for the words it records, the second press confirms, Cancel disarms (W/pages/money.rs:14-86).
| panel | rows and buttons |
| "Needs you · N" | one row per unfinished attempt: date, ⚠ line, Open checkout; Abandon… / Confirm: abandon it and release what it holds with reason (POST /api/purchase/abandon → Purchase.Abandon); → the tenant |
| "Paid at the vendor" | the payments the vendor reports that the ledger has not closed (GET /api/paid-at-vendor → ReconcileAdmin.ListPaidAtVendor) |
| "Owed back · N" (hint "Paid, no account made.") | Refund… / Confirm: refund the whole payment with reason (SignupAdmin.Refund); Refunded by hand… / Confirm: close it out with note (SignupAdmin.Settle) |
| "Paid signups · N" | SignupAdmin.List |
| "History" | one line per attempt (date, line, price, tax, mark), the steps behind a disclosure |
| "Spent at the clouds" | per provider: "default · purchases go here", the spend against the ceiling as a bar; an unbounded cap in words |
Tax, /e/<e>/tax (W/pages/tax.rs)
"Tax owed per country, as each payment recorded it. Nothing is filed for you: these are the figures to enter." Buttons ← / → step the year; the period buttons (GET /api/tax/periods?year= → Tax.Periods); the table "Tax return boxes" (Box, Label, Amount; GET /api/tax/report → Tax.Report); link Download .csv.
Offer, /e/<e>/offer (W/pages/products.rs:126-169)
"Prices, terms, and what a customer is offered." Panel "Prices" (Product, Country, Disk as "free" or "X / GiB / period"; the policy line "billed per … · N days of grace · kept N days after that"); panel "Bundles" (field Customer, button Show offer → Purchase.Bundles; table Bundle, Price, Can serve).
Events, /e/<e>/events (W/pages/ledger.rs:128-168)
"Newest first: refused payment messages and every ledger step." The refused webhook deliveries (CallbackAdmin.ListRefusals) and the panel "Ledger steps" (Ledger.Query).
Settings, /e/<e>/settings (W/pages/settings.rs)
"Current settings. They change with an install or upgrade." Panels "Payment keys" (Stripe live/test, every vendor), "Names" (Estate, Reached at, Serves, Clouds), "Keys" ("This console signs with" a fingerprint, or "no key: a loopback bearer"; "Answering"), "Passkeys" (label · operator · since · last used · DISABLED; Sign every session out… / Confirm: sign out everywhere → POST /auth/sessions/revoke-all), "Backup" (Back up now… / Confirm: export the ledger → Ledger.Export).
Accounts, /accounts and /account?product=&tenant= (W/pages/accounts.rs)
"Every account, by product and customer." (Accounts.List.) One account: table "Rows" (kind, plan / stop, host, last heard, billing, since) with the server-offered settles Set up again / Confirm: set it up again and Refund / Confirm: refund the order (POST /api/estate-orders/{company}/settle → EstateOrder.Settle; offered for a run in phase owed or held), a private row's Setup log / Hide the setup log (Accounts.InstanceLog); table "Payments" (when, reference, outcome, what).
Guardian, /guardian and /guardian/estate?instance= (W/pages/guardian.rs)
"Repair cases for every estate. Auto-fix is off unless turned on." Table "Estates" (estate, product, main, twin, auto-fix, open case; Guardian.Cases). One estate:
| element | what it does |
| panel "Boxes" | role, last heard, address |
| panel "Repair": buttons Recharge from twin, Review twin, New twin, New server (the recommended one styled danger; all disabled while a case is approved or running) | POST /api/guardian/{i}/press {remedy, case_id} → Guardian.Press (a strong session) |
| "Auto-fix: ON/OFF (opt-in)"; button Turn auto-fix on / Turn auto-fix off | Guardian.SetAutoFix |
| "Estate bearer: live until … / EXPIRED / REVOKED / none — revoke it at the door (attach-revoke)" | the estate's ONE bearer (365 days) |
| panel "Cases" (opened, finding, state, remedy, approved by, outcome); the newest case: steps (step, state, detail), evidence, log, receipt | |
| the update block (as on Sites) and the terminate block: "Terminate this estate: its machines, disks and DNS names go. Nothing is refunded. Type to confirm." button Terminate | POST /api/sites/terminate {estate, confirm} → EstateOrder.Terminate |
Test buy, /test-buy (W/pages/test_buy.rs)
"Buy as a customer would. A real account (and machines, for a private instance) is made. Each button says what it costs." One panel per product with the product's declared test actions (Gunnar: hosted-10gb, private-small; Holger: repository-10gb, private-small); the button reads " · " and adds " · LIVE — you will be charged" on a live key; disabled with its reason ("a test buy needs an admin passkey"). Press: POST /api/test-purchase/start → TestPurchase.Start (chapter 1 §1.2). Panel "Runs" (started, product, action, account, instance, state, where); a run shows "mail goes to …" and, on a live key, the link "LIVE — you will be charged: pay at the vendor's page".
Sites, /sites (W/pages/sites.rs)
"Your own sites. Ordering one sets it up; what it costs is on its row."
| element | what it does |
| panel "This box": button Update this box to (disabled: "an admin updates the box", "asked: the box is taking it") | POST /api/self/update → the box's self-update (chapter 2 §2.7) |
| panel "Certificates": per host the lines and Try now | POST /api/self/certs/try |
| panel "Plugins": button Update plugins; a table per plugin (unchanged / replaced / held / refused) | POST /api/plugins/update → Plugins.Update (chapter 2 §2.6) |
button New site → the form "New site" / "Edit ": Name, Domain, Product, First admin, One of my own sites, May be terminated, Stripe account, Mode test/live, Stripe secret key, Webhook secret ("keep the one held"), Term days, Term months, Grace hours, Margin basis points, Media; buttons Add site / Save, Cancel; <details> "Changes" |
POST /api/sites/put → SiteBook.PutSite (secrets are kept in <data>/solo/app-secrets.json, shown as a fingerprint) |
| per site: Edit; the state badge; "Running at https://…"; the link Pay while an order waits for payment; the settles (armed, then confirmed) | GET /api/sites/book; POST /api/estate-orders/{e}/settle |
| the order form (not ordered yet): Size, First admin's e-mail, Seed key (public, optional), button Order ; else "Cannot be ordered: …" | POST /api/sites/order → EstateOrder.OrderSite (EUR 0.50 at the page) |
| a ready site: Update to , the status line, the switch Live updates — on/off | EstateOrder.UpdateEstate, UpdateStatus, SetLiveUpdate |
| the product's own orders: each kind with its params and Queue; the answer | EstateOrder.ProductOrderKinds, ProductOrder, ProductOrderAnswer |
| Logs / Hide logs with "plain text" per machine | EstateOrder.NarrateLog |
| a terminable site: "Terminate this site: its machines, disks and DNS names go. Type to confirm." button Terminate | EstateOrder.Terminate |
Iron, /iron (W/pages/iron.rs)
"The cloud account: every estate's machines, disks and backups, and what no order owns." One panel per cloud account; per estate the owned rows (id, size, where, ref) and the backup lines; "Needs you · N" tasks (orphans, missing). (GET /api/iron → Products.List
Usage.Fleet.)
7.3 Sessions and sign-in
The session cookie is __Host-monetize_ui_session, HttpOnly, 12 h; the step-up window is the identity core's (S/auth.rs:37-61). Passkeys, the session MAC key and the revocation epoch live in <data>/monetize-ui/state.redb. The identity core (nornir-identity) is a private site, invite only, no mail: the inviter hands the link over. The break-glass token (console_admin_token, fed by the kit) enrols the FIRST operator only; the door's POST /passkeys/reset disables every passkey so it can be used again. With neither passkeys nor a token the console runs open and refuses to bind anywhere but loopback. A standalone monetize-ui-server has no operator client: refunds, settles, the site verbs, CODE RED, logs and plugins work only in the ONE box's console (S/main.rs:234-240; crates/monetize-admin/src/console.rs:103-115).
7.4 The command line: monetize
crates/monetize-cli. Every command is ONE request at the box's door :8475 over mutual TLS with the operator's installer certificate (--door/MONETIZE_DOOR, --material/MONETIZE_MATERIAL); the door forwards it to monetize with the box's operator bearer. "Every answer is the box's own; this tool decides nothing." The kit runs it as cargo xtask site|estate|box …. There is no feed verb: the feed is the kit's cargo xtask feed (PUT /feed), chapter 2 §2.4.
| verb | door → service |
site list |
GET /site/list → ListSites |
site order <site> [--size s] --admin <addr> [--seed-key <file>] [--return <url>] |
POST /site/order → OrderSite |
site test-order <site> [--size] --admin <addr> [--seed-key] |
POST /site/test-order: paid on the box with the vendor's TEST card, refused on a live key, no return page |
site status <site> |
Status |
site narrate <site> [<vm>] [--tail n]; site installer-log <site> <vm> [--since n] |
NarrateLog; InstallLog |
site recheck | retry | abandon <site> |
POST /site/settle → Settle ("Check again", "Set up again", "Abandon this install") |
site open-pin <site> |
Settle open-pin: every unpinned role's pin window opens without end; a pinned key is never replaced |
site cancel <site> |
Cancel |
site change-plan <site> [--main-plan] [--front-plan] [--data-gib n] [--set f=n]… [--test] |
ChangePlan |
site terminate <site> --confirm <name> |
Terminate; --backup is refused [terminate-no-backup]; nothing is refunded |
site first-link <site> [--to addr] |
FirstLink (the first sign-in link again) |
site refeed <site> [--only mail_relay|public_repositories|public_site]… |
Refeed (the refeed order) |
site kinds <site>; site do <site> <kind> [k=v]… [--file k=path] [--no-wait]; site do-answer <site> <seq> |
ProductOrderKinds, ProductOrder, ProductOrderAnswer (the product's own orders, e.g. Holger's downstream_invite) |
site show [<site>]; site add <site> --domain … --first-admin … [fields]; site edit <site> [fields] |
SiteBook.ShowSite / PutSite; fields: --domain --product --first-admin --operator-site yes|no --terminable yes|no --vendor stripe/<acct> --stripe-key-file --webhook-secret-file --mode test|live --term-days --term-months --grace-hours --margin-bp --media-from (secrets from files, never printed) |
estate update <e> [--release id] [--front-first]; estate update-status <e>; estate live-update <e> on|off; estate update-cancel <e> [--apply] |
UpdateEstate, UpdateStatus, SetLiveUpdate, CancelUpdate |
estate adopt <site> --front --main --twin --feed-out <file> [--machines] [--zone] [--release-of r=id]… [--again] |
AdoptEstate: an estate laid elsewhere is taken in; the feed is written 0600 to the file |
box running; box update [--apply]; box update --back [--apply]; box certs [--try host]; box plugins [--apply]; box inventory |
/self/running, /self/update, /self/certs, POST /box/plugins → Plugins.Update, GET /cloud/inventory |
estates |
GET /estates/overview → Estates.Overview |
front certs --at https://<site>/<door> (--token-file | --cookie-file) [--ca] [--try] |
the one command that asks a product front, not the ONE box |
The door's other routes the kit uses (crates/monetize-admin/src/door.rs:1868-1957): PUT/GET /feed (the GET returns key names only), PUT /feed/next, GET /feed/applied, GET /offer, POST /passkeys/reset, POST /attach/token, POST /attach/revoke, POST /restore, GET /money/log (the money lines, 2000 by default, ?tail=N up to 50 000).
7.5 The alert mail
Every line monetize writes for its operator starts ALERT <code> (chapter 9). PID 1 hears it, keeps it by its code and mails it to secret.alert_to through the box's own relay (secret.smtp_url): the first mail at once, then at most one per 10 min per code; code-red never waits and stays raised on the console's banner until the step that clears it succeeds (crates/monetize-admin/src/alerts.rs:1-45). The money lines (signup, callback, reconcile, nonpay, card check, payment, purchase, renewal, refund, site order, estate order, test run) are also kept at <data>/money/lines.log (8 MiB, rotated once).