Symptom

Three issues with Server.Pin nodes inside ProjectScreen’s Hardware section, all hitting the user immediately after they added a fresh unconfigured pin via the project dashboard’s + menu:

  1. Wrong icon for unconfigured pins. A pin with pinNumber = 0 (the default) showed up as a black HardwareStatusDot with a “0” in the middle. That’s the same visual the editor uses for configured pins — it suggested the pin had been pinned to GPIO 0, which would be wrong (and on a Pi GPIO 0 is reserved). On the ClientScreen swarm tree, the same node correctly rendered as a raspberry-pi icon (the unconfigured affordance).
  2. Click swallowed. Clicking the pin row in the dashboard did nothing. The user had no way to open the editor from ProjectScreen; they had to navigate back to ClientScreen, find the pin in the swarm tree, and edit from there.
  3. No spacing between hardware rows. Pin and serial-device rows were rendered as edge-to-edge Boxes with no padding, so they visually merged with each other and with the next section header.

Root cause

  1. EditPin.kt’s PinRow(id) always rendered HardwareStatusDot, regardless of whether the pin was configured. HardwareStatusDot reads meta.pinNumber and meta.color and renders a coloured sphere with the pin number — there is no “unconfigured” branch in it. The swarm tree dodged the bug because it goes through IconManager.NodeCompoundImage, which short-circuits the dot for unconfigured pins (hardwareId.isEmpty() || pinNumber == 0) and falls through to the painter-level icon (raspberry_pi_brands).
  2. HardwareStatusDot declared onClick: ((PinMetaData) -> Unit) = {} — a default no-op lambda — and unconditionally attached Modifier.clickable { onClick(meta) }. Inside ProjectScreen’s Hardware section the outer Box had its own .clickable { openEditor(...) }, but the inner clickable intercepted the gesture first and ran the no-op. The result: a click target that did precisely nothing.
  3. ProjectScreen’s Hardware section just forEach‘d each child into a bare Box. DashboardSection’s outer Column has no verticalArrangement, so siblings sit at 0dp from each other. The other sections happened to mask the issue because their child composables (Card-based LogicGateRow, SerialDeviceRow, etc.) carry their own internal padding; PinRow does not.

Fix

Prevention