M2 · Placement grid you can act on

Placement is the first thing an operator reads on the Dashboard, but each cell is a coloured span with a tooltip: it cannot be focused, cannot be clicked, does not show proxied copies, and says up or down by hue alone. This plan makes every cell a keyboard-reachable button with a glyph, adds a column per egress, and puts the fix one keypress away.

Mockup, dark theme, data fake. Colours and type from app/static/css/style.css on origin/main. Purple numbered circles are notes for the reader.

BEFORE The grid today

Current placement grid: two services, two boxes, colour squares

Restarting Honeygain on box-b, the day it dies

  • Cells are <span class="cell cell-fail" title=…> (app.js:1481-1485): no tabindex, no click handler. Tab skips the whole grid (screenshot 33-kbd-dash-tab15.png).
  • To act: scroll to the Services table, find Honeygain, expand its copies, find the box-b row, click Restart (app.js:2079-2141): 1 scroll + visual search + 2-3 clicks, the cause (exit code, OOM) is not on the way.
  • State is colour only: .cell-ok green vs .cell-fail red differ by luminance 1.16:1 (style.css:1433-1435), so red-green colour-blind users see two equal squares.
  • Proxied copies are dropped before placement sees them (orchestrator.py:3034-3038): a dead copy behind isp-1 never shows on the Dashboard.
  • A service whose account is disabled still shows green cells and an Up count; nothing says the copies earn nothing.
  • No “last updated” time: a grid frozen by a failed poll looks the same as a fresh one.

STATE 1 Dashboard, Placement with egress columns and glyphs

Same place on the Dashboard, same hairline style. New: glyphs, egress columns, account and link states, freshness.

CashPilot
+ Add service$148.32S

Dashboard

updated 12 s agoRefresh r
Balance
$148.32
Today
$0.94
This month
$21.40
Copies
11 up · 1 down · 1 unknown

Placement

✓ up! degraded× down? unknown· not placed🔒 account off⛓ pending link┆ via egress
Service box-a box-a via isp-1 box-b nas-…-2 Up
Honeygain✓✓×✓3/4
EarnApp✓n/a!⛓·1/2
Mysterium✓n/a?✓2/3
Repocket✓+✓2·3/3
🔒 IPRoyal Pawns✓✓··account off

Tab into the grid, arrows to move, Enter to act. Sorted worst first.

Needs attention 3

Resource use per server
box-a via isp-1
exits x.x.x.10 · Spain · Residential/ISP
open in Proxies & VPNs →
1 2 3 4 5 6 7 8
  1. 1Freshness next to Refresh. “updated 12 s ago”, counting up. After a failed poll or past 3 minutes it turns amber and says so; the existing r key (app.js:189) still refreshes.
  2. 2Glyph in every cell: ✓ up, ! degraded, × down, ? unknown (dashed, not filled, so it never reads as a failure), · not placed. State survives greyscale and colour-blindness; colour stays as the fast channel.
  3. 3One column per egress in use, dashed to say “a copy, another IP”. Header tooltip shows exit IP, country, IP type and links to the Proxies & VPNs page. Fed by proxied copies that orchestrator.py:3034-3038 currently filters out; app/worker_api.py reports them with their egress id.
  4. 4Pending device link: an amber chain on the cell when EarnApp, Bitping and similar run but the device is not yet claimed in the vendor's dashboard. The cell is degraded, not up, because it earns nothing yet.
  5. 5Several copies on one box show a small count, as the current .cell-n does.
  6. 6Account off. The whole row is hatched with a lock and “account off” replaces the Up count, so green containers on a disabled account no longer look like income. Uses the existing account-disabled flag from Needs attention.
  7. 7Middle-truncated box names (nas-…-2) so two boxes with the same long prefix stay tell-apart; today names are cut at the end at 14 characters (app.js:1468).
  8. 8Empty egress cell (Repocket via isp-1, shown here hovered) stays blank at rest and shows a faint “+” on focus or hover (State 3). n/a where the service cannot run through a proxy, with the reason in the tooltip.

STATE 2 Focused cell, popover with the cause and the fix

Enter on Honeygain × box-b. The popover reads the container state the worker already reports, and the actions call the same endpoints as the Services table buttons.

CashPilot
+ Add service$148.32S

Dashboard

updated 3 s agoRefresh r

Placement

↑↓←→ move · Enter act · Esc close
Servicebox-abox-a via isp-1box-bnas-…-2Up
Honeygain✓✓×✓3/4
EarnApp✓n/a!⛓·1/2
Mysterium✓?✓2/3
Repocket✓✓·3/3
1 2 3 4 5
  1. 1Visible focus ring on the cell. The grid is one Tab stop (roving tabindex): Tab enters on the worst cell, arrows move, Home/End jump to row ends. Screen readers hear “Honeygain on box-b, down, exited out of memory”.
  2. 2The cause, in words, first line. Exit code 137 plus the memory limit becomes “ran out of memory at 256 MB”. Restart count tells a crash loop from a one-off.
  3. 3Last 20 log lines inline, monospace, so most diagnoses need no second click. l opens the full log viewer that exists today.
  4. 4A suggested fix when the cause is known (OOM, port in use, bad credential, image pull failure), with the numbers from the fleet.
  5. 5Actions with keys, primary first by state: the suggested fix when the cause is known (here Edit settings, since starting it again at 256 MB repeats the out-of-memory stop), Start for a stopped copy with no known cause, Restart for a running but degraded one. Today's path to Start or Restart: scroll, search, expand, click (2-3 clicks plus a search). Now: Tab, arrows, Enter, s (Start). Copy as CLI (wave 3) gives cashpilot service start honeygain --box box-b.
Keyboard Tab enters the grid once, on the worst cell→ ↑↓←→ move→ Enter opens the popover, focus on its first action→ s / Shift R / l / d / e act→ Esc closes, focus returns to the cell

STATE 3 Enter on an empty egress cell: place it there

The empty cell under “box-a via isp-1” for Repocket is a shortcut to the placement dialog, already filled in, with the vendor reach result from the last egress test (M1, Screen 3).

CashPilot
$148.32S
Servicebox-abox-a via isp-1box-bnas-…-2Up
Repocket✓+✓·2/2
1 2 3 4
  1. 1Faint “+” only on focus or hover, so the resting grid stays quiet.
  2. 2Prefilled from the cell: service from the row, box and egress from the column, account from the copy that already runs. Today: Fleet → scroll → “Run a copy through a proxy” → three selects → Run (fleet.html:114-123).
  3. 3Vendor reach and traffic budget before committing, so a placement that would earn nothing or blow the cap is visible up front. A failed reach turns the button into “Place anyway” with the reason.
  4. 4Enter places; the cell turns to a loading glyph, then ✓ when the worker reports the copy. Esc returns focus to the cell. Enter, Enter instead of ~6 actions.

STATES Loading, error, empty, and phone

loading · first paint

Placementreading the fleet…

stale · last poll failed

Placementupdated 4 min ago

Amber after 3 min or a failed poll; title says which.

error · workers unreadable (app.js:1437-1439 today)

Placementupdated 6 min ago

Could not read the workers, so placement is unknown. Showing the last good grid from 6 min ago, dimmed. Retry r

empty · no services yet

Placement

Nothing placed yet.

Pick services and the boxes to run them on; they show up here as rows and columns.

+ Add service n

390 wide · phone

Placement12 s ago
Servicebox-avia isp-1box-bnas-…-2Up
Honeygain✓✓×✓3/4
🔒 Pawnsoff

Tap a cell: the popover opens as a bottom sheet with the same actions.

CLI · API The grid as data

$ cashpilot service ls --grid SERVICE box-a box-a/isp-1 box-b nas-attic-2 UP Honeygain ✓ ✓ × ✓ 3/4 EarnApp ✓ n/a !⛓ · 1/2 Mysterium ✓ n/a ? ✓ 2/3 Repocket ✓ · ✓2 · 3/3 IPRoyal Pawns account off updated 12 s ago · 1 down: cashpilot finding ls --box box-b $ cashpilot finding ls --box box-b --json { "service": "honeygain", "box": "box-b", "egress": null, "state": "down", "reason": "oom", "exit_code": 137, "mem_limit_mb": 256, "exited_ago_s": 240, "restarts_1h": 3, "suggest": { "action": "edit", "mem_limit_mb": 384 }, "log_tail": ["… 20 lines …"] }
  1. ASame glyphs in the terminal, so a screenshot from either reads the same. GET /api/placement returns the grid with proxied copies and account_disabled; the CLI and the Dashboard both render it.