M4 · PLAN ITEM · covers N8, X10

Add a box: a dialog that generates everything and waits live

A second box on a different IP is how the fleet earns more, and adding one is the step most likely to end in a terminal with no answer. The in-app snippet today has 3 of the 6 variables a remote worker needs, copies a loopback UI address no other machine can reach, sits in a closed <details> under everything else on Fleet, and never says whether the new box arrived.

Today

  • Find it: scroll past Boxes, Running costs and Proxies, open the last <details> (fleet.html:128-129). With the keyboard: about 30 Tabs from the top bar.
  • Fill it: 3 variables (fleet.html:137-139); the docs need 6 (docs/fleet.md:117-124): CASHPILOT_WORKER_URL, _WORKER_BIND_ADDR, _COMPOSE_DIR are missing, and the docs warn that without WORKER_URL the UI often cannot reach the worker (docs/fleet.md:133-134).
  • UI address: window.location.origin (fleet.html:171), which is http://127.0.0.1:8080 on a default install, because the UI is published on loopback only (docker-compose.yml:36).
  • Key: Reveal, then Copy the shared enrollment key, which stays valid on every future box (docs/fleet.md:137).
  • Then: 5+ terminal steps on the box (download, write .env, edit 6 lines, up, check logs), and the only sign of success is a new row appearing whenever Fleet next refreshes.

After

  • Find it: a + Add a box button in the Boxes header, n on Fleet, and "add box" in the palette (M3). A single-box fleet gets a one-line nudge.
  • Fill it: two fields. Everything else is generated: all 6 variables, the compose file pinned to this UI's version, a one-use join token that expires in 1 h.
  • UI address: chosen from addresses the UI can see, with a warning and the one-line fix when the UI only listens on loopback.
  • Then: 1 paste on the box. The dialog waits live, then runs 6 checks and shows the fix for any that fail, with Check again.
  • Terminal or agent: cashpilot box add box-c --print compose prints the same block; cashpilot box wait box-c --json streams the same checks.
Before · bottom of Fleet, details opened (screenshot of v2.1.1)
  • Loopback UI URL copied as is.
  • server-name is a placeholder to edit by hand.
  • No compose file, no docker command, no Unraid path.
  • Badge says "Configured", not "this box arrived".

Screen 1 · Fleet with one box: the button and the nudge

The entry point moves from the bottom of the page to the header of the table it adds to.

CashPilot
Add service
$184.32

Fleet

Boxes
1
Reporting
1
Containers (online)
7
Running (online)
7

Boxes 1

Each box runs one CashPilot worker.
BoxSystemVersionLast seenPower drawContainers or appsActions
box-a
wk_3f9c1e7a
Linux 6.8
x86_64 http://box-a.lan:8081
v2.1.1 4 s ago W honeygain · earnapp · traffmonetizer · packetstream · repocket · mysterium · storj
One box. Each extra box on a different IP earns again: most bandwidth apps pay per IP, not per account. Add a box →

Running costs

Costs are known for 1 of 1 machine.

box-a draws 18 W · €3.89 a month at €0.30/kWh · earns $41.07 a month.

Proxies

Copies of a service that leave through a proxy instead of the box's own connection.
1 2 3 4
Keys n on Fleet opens the dialog⌘K "add box" from any pageTab reaches the button 1st in the Boxes block, not 30th on the page
  1. 1Primary button in the Boxes header, with its key on it. Owner only, like the rest of Fleet's write actions (fleet.html:77). Replaces the closed <details> at fleet.html:128.
  2. 2Count in the header so "how many boxes" is read where they are listed.
  3. 3One-box nudge only when the fleet has exactly one Docker box. Says why in one line. "Not now" hides it for 30 days, per user.
  4. 4Same dialog from the nudge; no second path to keep in sync. Phones (CashPilot-android) keep their own install link inside the dialog.

Screen 2 · Step 1: name the box and pick the address it will call home on

Warning state: the UI is published on loopback only, so no other machine can reach it yet. The dialog says so before the user copies anything.

CashPilot
Add service
$184.32

Fleet

1 2 3 4 5 6
Keys Tab name → address → chips → own address → Next←→ move between chips (one radio group)↵ advances from any field⌘C on the focused block copiesesc closes, focus back on + Add a box
  1. 1Name checked as you type against existing boxes, so two rows with the same name (a case fleet.html:417 has to explain today) cannot start here.
  2. 2Detected addresses as chips: the UI host's interfaces (reported by the local worker), plus the Host of this request when it came through a reverse proxy. Loopback is shown but disabled, with the reason.
  3. 3The loopback warning is read from the published ports of cashpilot-ui as the local worker sees them (docker-compose.yml:36), not guessed from the browser URL.
  4. 4Two ways out, both in the dialog: the one .env line with Copy, or a reverse proxy URL. Never a silent "copy this loopback URL".
  5. 5Safety text next to the fix, matching the compose comment (docker-compose.yml:27-31): LAN or VPN address, not all interfaces.
  6. 6Optional own address becomes CASHPILOT_WORKER_URL and _BIND_ADDR; empty means the generated command detects it on the box, which avoids the Docker bridge address trap (docs/fleet.md:133-134).

Screen 3 · Step 2: one block per install method, everything filled in

The join token is created when this step opens; the dialog starts listening for it at the same moment.

CashPilot
Add service
$184.32

Fleet

1 2 3 4 5 6 7
Keys 1–4 or ←→ on the tab strip switch methodTab lands on the code block first⌘C / Ctrl C on the focused block copies all of it↵ goes to step 3⌫ back
  1. 1Tabs per install method, remembered per user. Compose first because it is the documented path (docs/fleet.md:110-125); the Android tab carries the phone path the old text linked to.
  2. 2Pinned to this UI's release (v2.1.1) instead of main as in docs/fleet.md:116, so the "versions match" check in step 3 passes by construction.
  3. 3All 6 variables, filled. The three the in-app snippet lacks today (fleet.html:137-139) are derived on the box, so nothing needs editing after paste.
  4. 4One large Copy per block, and ⌘C on the focused block. When the clipboard API is blocked on a plain-http origin, the block becomes a selected textarea, as the existing fallback does (fleet.html:147-152, 942).
  5. 5A join token, not the shared key. One use, 1 h, named for box-c. The shared enrollment key (docs/fleet.md:137) never has to leave the UI. "New token" revokes the old one.
  6. 6Copy as CLI (wave 3, L5) for people and agents who prefer a terminal: the same generator behind cashpilot box add.
  7. 7Already listening. Closing the dialog is safe; when box-c says hello a toast and the Fleet row appear anyway.

Screen 4 · Step 3: waiting, then a checklist that turns green line by line

Left: 42 s after copying, nothing yet. Right: box-c said hello; five checks run in order and one needs a fix.

CashPilot
Add service
$184.32

Fleet

1 2 3 4 5 6 7
Keys Focus jumps to the first failed line's Check again, so ↵ re-runs itTab Copy fix → Check again → Whyesc closes; checks keep running and post a toast
  1. 1A live clock in an aria-live="polite" region, announced at most every 15 s, so a screen reader is told it is still waiting without chatter.
  2. 2The whole checklist is visible from the start, greyed, so the user knows what "done" means before it arrives.
  3. 3Expectation set in words: how long a first pull can take, and when the dialog will change its mind.
  4. 4Title is the verdict: counts first, so a glance answers "did it work".
  5. 5Each failed line carries its own fix with the exact values, here the bridge-address trap the docs already warn about (docs/fleet.md:133-134).
  6. 6Lines that depend on a failed one say so ("runs once the UI reaches box-c") instead of failing too.
  7. 7Done stays disabled while a check fails; Copy report gives a support-ready text with no secrets (token shown as cpj_…W3mz).

Screen 5 · Failure after 2 minutes: no hello, most likely cause first

The dialog orders causes by what it already knows (here: the UI is still on loopback) and gives each one a command to run on box-c.

CashPilot
Add service
$184.32

Fleet

1 2 3 4 5
Keys Focus lands on the first fix's buttonTab walks fixes in order; each code block is one stop, ⌘C copies itesc closes and keeps listening
  1. 1Most likely cause first, chosen from facts the UI holds: its own published address, whether the token was ever presented, the clock.
  2. 2Stated as a checked fact, with the time it was checked, and a button that reopens the exact fix from step 1.
  3. 3One test that splits the problem in two, against the existing /api/health route (app/main.py:6798).
  4. 4Token state is part of the diagnosis: "unused, 58 min left" rules out expiry without anyone reading a log.
  5. 5Safe exits: Keep waiting is primary; Cancel revokes the join token so a half-made box cannot join later by surprise.

Screen 6 · Same flow from a terminal or an agent

For people who live in a shell and for agents such as Claude Code: the dialog's generator and checks behind two commands, with --json for machines.

$ cashpilot box add box-c --ui-url http://192.0.2.10:8080 --print env CASHPILOT_UI_URL=http://192.0.2.10:8080 CASHPILOT_API_KEY=cpj_8Hq2-Vn4x-Kd7r-W3mz CASHPILOT_WORKER_NAME=box-c CASHPILOT_WORKER_URL=http://$BOX_IP:8081 CASHPILOT_WORKER_BIND_ADDR=$BOX_IP CASHPILOT_COMPOSE_DIR=$PWD # join token: one use, expires 16:42 (1 h) # also: --print compose | docker-run | unraid-xml | qr $ cashpilot box add box-c --ui-url http://127.0.0.1:8080 refused: 127.0.0.1 is loopback; box-c cannot reach it. The UI is published on 127.0.0.1:8080 only. On the UI machine: echo 'CASHPILOT_BIND_ADDR=192.0.2.10' >> .env && docker compose up -d exit 5
$ cashpilot box wait box-c --timeout 5m waiting for box-c ... 0:42 ok heard from box-c 1:07 Linux 6.8 x86_64 fail UI can reach worker URL timeout 5 s box-c reports http://<bridge-ip>:8081 (Docker bridge). fix on box-c: set CASHPILOT_WORKER_URL=http://192.0.2.23:8081 ok versions match 2.1.1 ok socket proxy in use ok LAN fence on skip isolation check needs the line above exit 1 $ cashpilot box wait box-c --json | jq '.checks[] | select(.ok==false)' {"id": "worker_url_reachable", "ok": false, "fix": "set CASHPILOT_WORKER_URL=http://192.0.2.23:8081"}
1 2 3
  1. 1Same generator as the dialog's tabs; --print picks the tab. Needs a token with the admin scope (M7).
  2. 2Same loopback guard as the warning in step 1, as an exit code an agent can act on.
  3. 3Same six checks, same ids (worker_url_reachable …) in the API, the CLI and the dialog, so docs and agents can name them.

Mockup only. Addresses are documentation ranges (192.0.2.0/24, 198.51.100.0/24), names and tokens are fake. Colours, type, buttons, modals and tables use CashPilot's own stylesheet from origin/main (Classic night, the default theme), inlined. Yellow numbered dots are annotations, not UI.