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.
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.
1234
Keysn on Fleet opens the dialog⌘K "add box" from any pageTab reaches the button 1st in the Boxes block, not 30th on the page
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.
2Count in the header so "how many boxes" is read where they are listed.
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.
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.
Shown in Fleet and in every list. box-a is taken. Lowercase, digits and dashes.
Your UI listens on 127.0.0.1 only. Remote boxes cannot reach it.
The cashpilot-ui container is published on 127.0.0.1:8080, the safe default. box-c will not be able to call http://192.0.2.10:8080 until the UI is published there too, or put behind a reverse proxy.
# On the machine running the UI, in the folder with its docker-compose.yml:echo'CASHPILOT_BIND_ADDR=192.0.2.10' >> .env
docker compose up -d
Publish on the LAN or VPN address only, never 0.0.0.0 on a machine with a public IP. This page reloads by itself once the UI answers on the new address.
Where the UI reaches box-c's worker. Left empty, the command finds box-c's first LAN address itself.
Nothing is created until step 2.
123456
KeysTab 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
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.
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.
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.
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".
5Safety text next to the fix, matching the compose comment (docker-compose.yml:27-31): LAN or VPN address, not all interfaces.
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.
Two containers: the worker never mounts the Docker socket itself.
tab 3 · Unraid
Apps → search CashPilot Worker → Install, then paste each field:
UI URLhttp://192.0.2.10:8080
API key (join token)cpj_8Hq2-Vn4x-…
Worker namebox-c
Worker URLhttp://[your Unraid IP]:8081
+ 2 more. "Copy all as XML" fills the template at once.
tab 4 · Android phone
A QR code with the UI URL and the join token. Scan it in the CashPilot Android app under Settings → Join a fleet.
1234567
Keys1–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
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.
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.
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.
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).
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.
6Copy as CLI(wave 3, L5) for people and agents who prefer a terminal: the same generator behind cashpilot box add.
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.
A first start pulls about 120 MB of images; on a slow line that alone can take a minute. If nothing arrives in 2 minutes this turns into a list of likely causes.
hello
box-c is in · 4 of 6 passed, 1 needs you
✓ Name and address
✓ Run it
3 Wait and check
Heard from box-c1:07 · Linux 6.8 x86_64
UI can reach the worker URLtimeout 5 s
box-c says its address is http://<bridge-ip>:8081, a Docker bridge address no other machine can reach. On box-c, set its LAN address and restart:
sed -i 's#^CASHPILOT_WORKER_URL=.*#CASHPILOT_WORKER_URL=http://192.0.2.23:8081#' .env
docker compose -f docker-compose.worker.yml up -d
Isolation check passedruns once the UI reaches box-c
1234567
KeysFocus 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
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.
2The whole checklist is visible from the start, greyed, so the user knows what "done" means before it arrives.
3Expectation set in words: how long a first pull can take, and when the dialog will change its mind.
4Title is the verdict: counts first, so a glance answers "did it work".
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).
6Lines that depend on a failed one say so ("runs once the UI reaches box-c") instead of failing too.
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.
No heartbeat after 2:00. Likely: the UI URL is unreachable from box-c.
The UI still listens on 127.0.0.1 only. Checked just now: cashpilot-ui is published on 127.0.0.1:8080. Fix it on the UI machine (step 1 has the line), then box-c retries by itself every 30 s.
Test from box-c: if this prints ok, the network is fine and the cause is below.
curl -fsS -m 5 http://192.0.2.10:8080/api/health && echo ok
The worker started but cannot authenticate. Look for 401 or join token in its log. The token is still valid for 58 min.
docker logs cashpilot-worker --tail 30
The containers never started (no Docker Compose v2, or the image pull failed): docker compose -f docker-compose.worker.yml ps on box-c.
Still listening for box-cturns green the moment it arrives2:14
12345
KeysFocus lands on the first fix's buttonTab walks fixes in order; each code block is one stop, ⌘C copies itesc closes and keeps listening
1Most likely cause first, chosen from facts the UI holds: its own published address, whether the token was ever presented, the clock.
2Stated as a checked fact, with the time it was checked, and a button that reopens the exact fix from step 1.
3One test that splits the problem in two, against the existing /api/health route (app/main.py:6798).
4Token state is part of the diagnosis: "unused, 58 min left" rules out expiry without anyone reading a log.
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:8080refused: 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"}
123
1Same generator as the dialog's tabs; --print picks the tab. Needs a token with the admin scope (M7).
2Same loopback guard as the warning in step 1, as an exit code an agent can act on.
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.