M8 · Safe confirm dialog and dry-run plan stateful actions

Mockup on origin/main · v2.1.1 · default theme “Classic night” · fake data
Today before
cashpilot.local:8080 says
Remove mysterium from box-a? Its container is stopped and deleted.
CancelOK
Enter ↵
cashpilot.local:8080 says
Also delete its data volume on box-a? This cannot be undone. Cancel keeps the data for a later deploy.
CancelOK
Enter ↵
mysterium removed from box-a
identity volume deleted too · no undo
Reconstructed from app/static/js/app.js:4172-4175. Native confirm() focuses OK, so two Enter presses delete the data volume with allow_delete_critical=true. For a Mysterium node that is the identity and its earnings history; for Storj, the node and its held amount.
  • !
    Remove + delete volume: 2 keystrokes (Enter, Enter). The safe answer is the non-default button both times (app.js:4172-4175).
  • !
    Stop a Storj or Mysterium node: 1 click, no confirm (app.js:2687-2697). Every minute down counts against the node’s uptime and audit score.
  • !
    23 confirm() calls in the client (app.js ×15, fleet.html ×5, activity.html ×2, import.js ×1, counted with git grep -n "confirm("). None shows what will happen, none can name a volume, none is themed or announced to screen readers beyond the browser default, and the text cannot be selected or copied.
  • !
    The modals that do exist are .modal-overlay divs (style.css:730-745): no focus trap, the page behind stays tabbable (walkthrough shot 35-kbd-modal.png: Tab leaves the dialog into the catalog rows).
  • !
    Redeploy says only “with its saved settings?” (app.js:4139, :3736, :3760). If the catalog moved Mysterium’s UDP range, the router forward silently breaks.
  • →
    After: one CP.confirm(plan) on a native <dialog>. Focus lands on Cancel; keeping data is the default; deleting needs the slug typed. Removing and keeping the volume: Tab, Enter (2 keys, deliberate). Deleting: 3 arrow/tab keys + type the slug + Enter.
Frame 1 · Remove Mysterium from box-a after · default state 1440×900
CashPilot
+ Add service$41.87O
Page behind is inert: Tab and the screen reader cannot reach the rows.
Tab / ⇧Tab cycles Cancel → Remove container → Data radios, inside the dialog only↑↓ changes the Data choiceEnter activates the focused button (Cancel on open)Esc cancels and returns focus to the row’s Remove button
Frame 1 · Delete chosen, typing to confirm, loading and error states
4
Partial slug: Delete volume is red and disabled. Enter in the field does nothing yet.
Exact match: enabled. Pasting is allowed; case must match. Picking Keep again clears the field.
5
Frame 2 · Stop Storj on box-b, then the undo toast after
6
Stopped Storj on box-b.Start again⨯
The toast lives in an aria-live="polite" region, so it is read aloud. It stays while hovered or focused (today toasts vanish on a timer, app.js:44-69). Tab reaches it; Enter starts the node again.
Frame 3 · Redeploy with a diff plan after
7
Today the same action reads “Redeploy mysterium on this box? It keeps the settings it was deployed with.” (app.js:3736), with no word on ports.
Frame 4 · The same plan from the CLI after
  zsh — cashpilot
$ cashpilot service rm mysterium --box box-a --dry-run --json
{
  "action": "remove",
  "service": "mysterium",
  "box": "box-a",
  "stateful": true,
  "steps": [
    { "what": "container", "name": "cashpilot-mysterium",
      "effect": "remove" },
    { "what": "volume", "name": "cashpilot_mysterium_data",
      "size_bytes": 12582912, "effect": "keep",
      "delete_with": "--delete-volume=mysterium" }
  ],
  "backup": { "confirmed_at": "2026-11-05T09:14:00Z", "current": true },
  "plan_id": "pln_8c1e4a",
  "expires_in": 300
}

$ cashpilot service rm mysterium --box box-a
Remove Mysterium from box-a?
  container  cashpilot-mysterium       remove
  volume     cashpilot_mysterium_data  keep (12 MB, backed up 3 d ago)
Not a terminal and no --yes: nothing done.
$ echo $?
5   # needs --yes

$ cashpilot service rm mysterium --box box-a --yes --delete-volume=storj
✗ --delete-volume must name this service (mysterium). Nothing done.  exit 2 (usage)
  • 8
    One plan, three surfaces. The dialog, --dry-run --json and the MCP tool all render the same object from _svc_remove(…, dry_run=True) next to the existing delete_volumes path (app/main.py:3837-3892).
  • 9
    No silent action from a script: without a TTY and without --yes the CLI prints the plan and exits 5. On a TTY it asks, with “No” as the default ([y/N]), the same safe default as the dialog.
  • 10
    The volume flag repeats the slug, the CLI form of “type mysterium to confirm”. A copy-pasted command for the wrong service fails with exit 2 (usage), before any guard runs.
  • 11
    For agents: the HTTP API returns the plan and a short-lived plan_id; the destructive call must send it back. An agent cannot skip the plan, and the owner sees the plan in Activity.
Every confirm() on origin/main and what it becomes
WhereToday asksBecomesDefault focus
app.js:4172, :4174Remove custom service from box; then “Also delete its data volume?”Frame 1 plan, Keep default, type slug to deleteCancel
app.js:2750Remove {slug}? stop and delete the containerFrame 1 plan (volume kept unless chosen)Cancel
app.js:2687 (none)Stop, no confirmFrame 2 for stateful services onlyCancel
app.js:2710Stop {name} on N containers?Frame 2, lists the boxes, warns per stateful oneCancel
app.js:3736, :3760, :4139Redeploy … with its saved settings?Frame 3 diff planCancel
fleet.html:533, :551Redeploy behind the LAN fence / turn on isolationFrame 3 diff plan (network changes)Cancel
fleet.html:793Remove workerPlan: what stays on the box, how to re-enrolCancel
fleet.html:1134, :1144Remove proxied copy / delete proxyPlan: copies using this proxy, RemoveCancel
app.js:2737, import.js:205Forget a deployment / imported settingsShort dialog, no plan (nothing on a box changes)Forget
app.js:845, :929, :4155, :4467, :4684, :3320, :3725; activity.html:327, :345Cash-out, payout, custom definition, credentials, deploy-anyway warning, self-update, ignore drift, activity recordsShort dialog with the record named; destructive labelCancel