Install
$ agentstack add skill-nerdbase-by-stark-skill-forge-mass-deploy-ux ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Mass-Deploy UX — Parallel Device Commissioning
Patterns for building UIs that orchestrate 50-1000 parallel operations against physical devices (PDUs, switches, APs, IoT fleets, MDM-managed endpoints) without overwhelming the technician running the deployment.
Pairs with pyside6-desktop (Qt widget patterns) and network-device-discovery (underlying parallel-deploy engine). This skill covers the interaction design layer.
Core design principle
A technician running a 120-device commissioning deployment is anxious, possibly on a deadline, and can't hold 120 device states in their head. The UI's job is to:
- Collapse state into status lanes — never show a flat list of 120 equal things
- Surface only actionable information — raw stack traces ≠ useful
- Survive shift changes and crashes — state must persist to disk
- Keep running when things fail — never halt on a single-device error
- Always tell the user what's next — ETA, throughput, count summary always visible
What follows are 10 concrete patterns from tools that got this right, each with a PySide6 implementation hint and the anti-pattern to avoid.
Pattern 1: Status-Lane Sorted Grid with Per-Item Progress
Seen in: Buildkite parallel job groups, Jenkins Blue Ocean, GitHub Actions matrix, Kubernetes Lens, Cisco DNA Center
Each device is one row. Columns: device name/ID, current status (Queued/Running/Complete/Failed), per-device progress bar, ETA, last error (if any). Rows are sorted by status so failures float to the top, not buried mid-list.
# PySide6 implementation hint
class DeviceGridView(QTableWidget):
STATUS_ORDER = {"failed": 0, "running": 1, "queued": 2, "complete": 3}
def _refresh(self):
items = sorted(self._devices, key=lambda d: (
self.STATUS_ORDER[d.status],
d.hostname,
))
for row, device in enumerate(items):
self._set_row(row, device)
def _set_row(self, row, device):
color = {
"failed": QColor("#fce4e4"),
"running": QColor("#fff4d6"),
"queued": QColor("#f1f3f4"),
"complete": QColor("#e6f4ea"),
}[device.status]
for col in range(self.columnCount()):
item = self.item(row, col)
if item:
item.setBackground(color)
Anti-pattern: Inserting new rows as devices finish (order jumps around). Always sort the full list on each refresh.
Anti-pattern: Re-rendering the whole table on every progress tick. Batch UI updates with a QTimer at 500ms — faster than the eye sees anyway.
Pattern 2: Throughput-Based ETA (Rolling Window)
Seen in: Ansible Tower (elapsed per play), Buildkite (estimated runtime), Jenkins Blue Ocean (stage duration)
Showing "ETA: 14:32" beats "ETA: 6m 12s" after 20 minutes — absolute times don't drift. Calculate from a 5-minute rolling throughput window, not from inception (otherwise a slow start dominates the estimate forever).
class ThroughputEstimator:
def __init__(self, window_seconds: int = 300):
self._samples: deque[tuple[float, int]] = deque()
self._window = window_seconds
def record(self, completed: int) -> None:
now = time.monotonic()
self._samples.append((now, completed))
# Prune samples older than window
while self._samples and now - self._samples[0][0] > self._window:
self._samples.popleft()
def devices_per_minute(self) -> float | None:
if len(self._samples) datetime | None:
rate = self.devices_per_minute()
if not rate or rate str:
if isinstance(exc, (ConnectionError, socket.timeout)):
return "Network"
if isinstance(exc, AuthenticationError):
return "Auth"
if isinstance(exc, (TimeoutError, requests.Timeout)):
return "Timeout"
if isinstance(exc, ConfigValidationError):
return "Config"
return "Unknown"
Anti-pattern: Halting the deploy on first failure. Parallel commissioning assumes some devices will fail; the operator must be able to triage without stopping the other 119.
Anti-pattern: Showing raw Python tracebacks to the tech. They're electricians, not Python devs. Classification → action ("retry" / "skip" / "investigate") beats stack trace every time.
Pattern 4: Persistent Worklist State (Atomic, Resumable)
Seen in: Ansible Tower job history, GitHub Actions workflow runs, Intune device provisioning log, MDM platforms
Commissioning 120 devices takes multiple hours, possibly across shift changes. If the app crashes at device 87, you need to resume at 88, not restart. Persist state after every single transition:
def mark_device(self, device_id: str, status: str, **metadata):
self._devices[device_id].status = status
self._devices[device_id].metadata.update(metadata)
self._devices[device_id].updated_at = datetime.now().isoformat()
self._persist_atomic()
def _persist_atomic(self):
"""Write to temp file, then rename — never leaves half-written JSON."""
path = self._persist_path
tmp = path.with_suffix(".json.tmp")
tmp.write_text(self._worklist.model_dump_json(indent=2), encoding="utf-8")
tmp.replace(path) # atomic on POSIX and Windows ≥ Vista
On app start:
def load_or_new(path: Path) -> WorkList:
if path.exists():
wl = WorkList.model_validate_json(path.read_text(encoding="utf-8"))
# Reset any "running" devices back to "queued" — they were in-flight when we crashed
for dev in wl.devices:
if dev.status == "running":
dev.status = "queued"
dev.attempt_count += 1
return wl
return WorkList.new()
Critical details:
- Atomic write (temp + rename) prevents corruption on crash mid-write
- JSON, not CSV — CSV parsing across app versions is fragile
- Persist on every transition, not batched — the 10ms cost is cheaper than losing 2 hours of work
- Reset "running" to "queued" on load — assume anything mid-flight at crash time is uncertain
Anti-pattern: Writing on a timer (every 30s). If you crash 29s in, you lose 29s of state. Write on every state change.
Anti-pattern: Pickling the worklist. Pickle doesn't survive Python version upgrades or code refactors. Always use a documented, stable text format.
Pattern 5: Physical-to-Logical Device Identification (LED Locate)
Seen in: Ubiquiti UniFi Controller, Cisco switches (locate CLI command), APC NetBotz sensor pods
120 PDUs in a rack look identical. When a device fails, the tech needs to find the physical one. Every device control API with an LED should expose a "locate" command; the UI should expose it per-device:
class DeviceRow(QWidget):
def _on_locate_clicked(self):
client = ApiClient(host=self._device.ip)
try:
client.blink_led(port=1, duration_seconds=30)
self.show_toast(f"Blinking port-1 LED on {self._device.hostname} for 30s")
except Exception as e:
self.show_error(f"Could not trigger LED: {e}")
finally:
client.close()
# After a delay, ask the tech to confirm
QTimer.singleShot(30_000, lambda: self._ask_located())
def _ask_located(self):
reply = QMessageBox.question(self, "Locate",
f"Did you see {self._device.hostname} blink?",
QMessageBox.Yes | QMessageBox.No | QMessageBox.Retry)
if reply == QMessageBox.Yes:
self._device.physical_confirmed = True
Anti-pattern: LED blink without confirmation loop. The tech may have missed it, or it's the wrong device — ask.
Anti-pattern: Too-fast or too-slow blink. 1 Hz for 30s is the sweet spot — human-perceivable, not annoying, long enough to get to the rack.
Anti-pattern: Locating without physical context. Print a static rack-position label on device stickers or show a diagram — "port 1 LED" means nothing if the tech doesn't know which port is 1.
Pattern 6: Compact Grid Heatmap (Spot Patterns, Not Just Individuals)
Seen in: Kubernetes Lens pod grid, Buildkite matrix view, GitHub Actions matrix, APC NetBotz sensor heatmap
A scrollable table of 120 devices is useful for "find PDU-045". A 12 × 10 colored grid is useful for "are all devices in Rack A failing?" Both have their place — offer the grid as an alternative view.
Each cell = one device, ~60×60px, color = status, hover shows IP/progress, click opens detail. Layout order: by rack/hostname, not by status (so geographic patterns emerge).
class DeviceHeatmapWidget(QWidget):
CELL_SIZE = 60
COLS = 12
def paintEvent(self, event):
painter = QPainter(self)
for i, device in enumerate(self._devices):
col, row = i % self.COLS, i // self.COLS
rect = QRect(col * self.CELL_SIZE, row * self.CELL_SIZE,
self.CELL_SIZE - 2, self.CELL_SIZE - 2)
painter.fillRect(rect, self._color_for(device.status))
painter.drawText(rect, Qt.AlignCenter, str(device.index + 1))
def mousePressEvent(self, event):
col = event.x() // self.CELL_SIZE
row = event.y() // self.CELL_SIZE
idx = row * self.COLS + col
if 0 5 devices.
**Anti-pattern:** Raw "OK / Cancel" with no device list. The tech has no idea what they're about to do.
**Anti-pattern:** Listing 120 devices in a modal with no scroll. Cap the preview at 20 rows with "...and N more."
## Pattern 10: Sticky Status Header with Abort Button
**Seen in:** Buildkite build header, Jenkins Blue Ocean pipeline header, GitHub Actions workflow page, Ansible Tower job page, Intune enrollment page
A top bar that **never scrolls off**, always showing:
- Deployment name + timestamp
- Count summary with glyphs: `45/120 ✓ | 15 ⧗ | 5 ✗`
- Rolling ETA (Pattern 2): "14:32 (3.2/min)"
- `[Stop]` button — opens confirmation modal before calling `deploy_engine.abort()`
```python
class DeployHeader(QWidget):
def __init__(self, deploy_engine: DeployEngine):
super().__init__()
layout = QHBoxLayout(self)
self._name = QLabel(deploy_engine.name)
self._summary = QLabel()
self._eta = QLabel()
self._stop = QPushButton("Stop Deployment")
self._stop.clicked.connect(self._confirm_stop)
layout.addWidget(self._name)
layout.addStretch()
layout.addWidget(self._summary)
layout.addWidget(self._eta)
layout.addWidget(self._stop)
# Refresh every 500ms
self._timer = QTimer(self)
self._timer.timeout.connect(self._refresh)
self._timer.start(500)
def _refresh(self):
s = self._engine.summary()
self._summary.setText(f"{s.complete}/{s.total} ✓ | {s.running} ⧗ | {s.failed} ✗")
eta = self._engine.eta()
self._eta.setText(eta.strftime("%H:%M") if eta else "…")
def _confirm_stop(self):
reply = QMessageBox.question(self, "Stop deployment?",
f"{self._engine.summary().running} devices are currently deploying. "
"Stop now will leave them in an inconsistent state.",
QMessageBox.Ok | QMessageBox.Cancel)
if reply == QMessageBox.Ok:
self._engine.abort()
Anti-pattern: [Stop] without confirmation. It's an irreversible action that can leave devices half-configured.
Anti-pattern: Hiding the count summary behind a tab. The whole point is constant visibility.
Anti-pattern: Running the refresh timer at 50ms (UI flicker) or 5s (too stale). 500ms is the human-visible sweet spot.
Prioritization for a new tool
If starting from scratch, implement patterns in this order:
| Tier | Patterns | Reason | |---|---|---| | 1 (day-1) | 1, 4, 10 | Status grid, persistent state, header — the skeleton | | 2 (week-1) | 3, 7 | Error triage, auto-fallback — essential resilience | | 3 (month-1) | 2, 9 | Rolling ETA, batch confirmations — ergonomic polish | | 4 (later) | 5, 6, 8 | Locate, heatmap, filter sidebar — scale to 100s of devices |
Patterns 1, 4, 10 together convert a "works" tool into a "shippable" tool. Everything else is refinement.
Anti-pattern summary
From worst to least-bad (all seen in real vendor tools):
- Halting the whole deployment on first error — kills multi-hour work for one bad device
- No persistent state — crashes force full restart
- Raw tracebacks in the UI — useless to non-dev techs
- Modal dialogs during deploy — freezes the UI, tech can't monitor
- No ETA / throughput — "is this ever going to finish?"
- LED identify without software integration — forces separate vendor CLI
- Flat error list, no grouping — tech scrolls through 120 items looking for 7 failures
- Manual worklist rebuild for resume — makes re-running effectively "do it again"
- No preview for bulk actions — accidental "Retry All" disasters
- Overly small grid cells or unsortable tables — forces endless scrolling
Sources
- Ansible Tower: Job output & host events
- Buildkite: Dashboard walkthrough, Retry failed jobs
- Jenkins Blue Ocean: Pipeline run details
- GitHub Actions: Matrix strategy docs
- Kubernetes Lens: Spacelift overview
- Kubernetes K9s: k9scli.io
- Microsoft Intune: Bulk enrollment
- Ubiquiti UniFi: Device LED indicators
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: NerdBase-by-Stark
- Source: NerdBase-by-Stark/skill-forge
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.