From 5ddb8973e280b10ded1f8535d7291f061db1b4d1 Mon Sep 17 00:00:00 2001 From: jedarden Date: Sat, 28 Mar 2026 02:36:58 -0400 Subject: [PATCH] feat(dashboard): interactive onboarding wizard for ESP32-S3 node provisioning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Web Serial-based wizard that takes a non-technical user from unboxed ESP32-S3 to streaming CSI in under 5 minutes. 8-step state machine: browser check → connect → flash firmware (esp-web-tools) → provision WiFi → detect node → guided calibration → placement guidance → complete. Includes sessionStorage persistence for resumability across page refreshes, live CSI waveform during calibration, human-friendly error messages for all failure modes, and comprehensive Jest test coverage (34 tests). Co-Authored-By: Claude Opus 4.6 --- dashboard/index.html | 383 +++ dashboard/jest.config.js | 5 + dashboard/js/onboard.js | 996 ++++++ dashboard/js/onboard.test.js | 540 +++ dashboard/js/onboard.test.setup.js | 80 + dashboard/package-lock.json | 4134 +++++++++++++++++++++++ dashboard/package.json | 11 + mothership/cmd/mothership/main.go | 33 + mothership/internal/fleet/fleet_test.go | 389 +++ mothership/internal/fleet/handler.go | 105 + mothership/internal/fleet/manager.go | 16 + 11 files changed, 6692 insertions(+) create mode 100644 dashboard/jest.config.js create mode 100644 dashboard/js/onboard.js create mode 100644 dashboard/js/onboard.test.js create mode 100644 dashboard/js/onboard.test.setup.js create mode 100644 dashboard/package-lock.json create mode 100644 dashboard/package.json create mode 100644 mothership/internal/fleet/fleet_test.go create mode 100644 mothership/internal/fleet/handler.go diff --git a/dashboard/index.html b/dashboard/index.html index dab847d..1901773 100644 --- a/dashboard/index.html +++ b/dashboard/index.html @@ -388,6 +388,384 @@ height: 80px; display: block; } + + /* ===== Onboarding Wizard ===== */ + #wizard-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.85); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + } + + #wizard-card { + background: #1e1e3a; + border-radius: 12px; + padding: 28px 32px 20px; + max-width: 560px; + width: 92%; + max-height: 90vh; + overflow-y: auto; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); + } + + #wizard-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + } + + #wizard-header h1 { + font-size: 20px; + font-weight: 600; + color: #eee; + } + + .wizard-close { + background: none; + border: none; + color: #888; + font-size: 24px; + cursor: pointer; + padding: 0 4px; + line-height: 1; + } + + .wizard-close:hover { + color: #eee; + } + + /* Step indicator */ + #wizard-steps { + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 24px; + gap: 0; + } + + .wizard-step-dot { + width: 28px; + height: 28px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + font-weight: 600; + background: #333; + color: #666; + transition: all 0.3s; + flex-shrink: 0; + } + + .wizard-step-dot.active { + background: #4fc3f7; + color: #fff; + box-shadow: 0 0 12px rgba(79, 195, 247, 0.5); + } + + .wizard-step-dot.completed { + background: #4caf50; + color: #fff; + } + + .wizard-step-line { + width: 20px; + height: 2px; + background: #333; + flex-shrink: 0; + } + + .wizard-step-line.completed { + background: #4caf50; + } + + /* Content */ + #wizard-content { + min-height: 180px; + } + + .wizard-step-content { + text-align: center; + } + + .wizard-step-content h2 { + font-size: 18px; + margin-bottom: 8px; + color: #eee; + } + + .wizard-step-content p { + color: #bbb; + font-size: 14px; + line-height: 1.5; + margin-bottom: 12px; + } + + .wizard-muted { + color: #777 !important; + font-size: 13px !important; + } + + .wizard-center-msg { + padding: 24px 0; + text-align: center; + } + + .wizard-center-msg p { + color: #888; + margin-top: 12px; + } + + .wizard-error { + color: #ef5350; + font-size: 13px; + padding: 10px; + background: rgba(239, 83, 80, 0.1); + border-radius: 4px; + text-align: left; + margin-top: 8px; + } + + .wizard-success { + color: #66bb6a; + font-size: 14px; + font-weight: 500; + } + + .wizard-warn { + color: #ffa726; + font-size: 13px; + } + + .wizard-icon-large { + font-size: 48px; + margin-bottom: 12px; + } + + .wizard-success-icon { + color: #66bb6a; + } + + .wizard-list { + text-align: left; + color: #bbb; + font-size: 13px; + line-height: 1.8; + margin: 12px 0; + padding-left: 20px; + } + + .wizard-list li { + margin-bottom: 4px; + } + + .wizard-details { + text-align: left; + margin: 12px 0; + } + + .wizard-details summary { + color: #888; + font-size: 13px; + cursor: pointer; + } + + .wizard-details summary:hover { + color: #bbb; + } + + /* Spinner */ + .spinner { + display: inline-block; + width: 32px; + height: 32px; + border: 3px solid rgba(79, 195, 247, 0.2); + border-top-color: #4fc3f7; + border-radius: 50%; + animation: wizard-spin 0.8s linear infinite; + } + + @keyframes wizard-spin { + to { transform: rotate(360deg); } + } + + /* Progress bar */ + .wizard-progress { + margin: 16px 0; + } + + .progress-bar { + width: 100%; + height: 8px; + background: #333; + border-radius: 4px; + overflow: hidden; + } + + .progress-fill { + height: 100%; + background: linear-gradient(90deg, #4fc3f7, #29b6f6); + border-radius: 4px; + transition: width 0.3s; + width: 0%; + } + + .wizard-progress p { + font-size: 12px; + color: #888; + margin-top: 6px; + text-align: center; + } + + /* Buttons */ + .wizard-btn { + padding: 10px 24px; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + border: none; + transition: background 0.2s, opacity 0.2s; + } + + .wizard-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + .wizard-btn-primary { + background: #4fc3f7; + color: #1a1a2e; + } + + .wizard-btn-primary:hover:not(:disabled) { + background: #29b6f6; + } + + .wizard-btn-secondary { + background: rgba(255, 255, 255, 0.1); + color: #ccc; + border: 1px solid rgba(255, 255, 255, 0.2); + } + + .wizard-btn-secondary:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.15); + } + + #wizard-nav { + display: flex; + justify-content: space-between; + margin-top: 20px; + gap: 12px; + } + + /* Form */ + .wizard-form { + text-align: left; + margin-top: 16px; + } + + .form-group { + margin-bottom: 12px; + } + + .form-group label { + display: block; + font-size: 13px; + color: #aaa; + margin-bottom: 4px; + } + + .form-group input { + width: 100%; + padding: 8px 12px; + background: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 4px; + color: #eee; + font-size: 14px; + box-sizing: border-box; + } + + .form-group input:focus { + outline: none; + border-color: #4fc3f7; + box-shadow: 0 0 0 2px rgba(79, 195, 247, 0.2); + } + + .form-group input::placeholder { + color: #555; + } + + /* ESP32 illustration */ + .esp32-illustration { + margin: 16px auto; + text-align: center; + } + + /* Calibration phases */ + .calibrate-phase { + margin: 8px 0; + } + + .calibrate-phase-number { + font-size: 11px; + color: #4fc3f7; + text-transform: uppercase; + letter-spacing: 1px; + margin-bottom: 4px; + } + + .calibrate-phase h3 { + font-size: 16px; + color: #eee; + margin-bottom: 6px; + } + + .calibrate-phase p { + color: #bbb; + font-size: 14px; + } + + /* Add Node button in status bar */ + #add-node-btn { + background: rgba(79, 195, 247, 0.15); + border: 1px solid rgba(79, 195, 247, 0.4); + color: #4fc3f7; + font-size: 12px; + padding: 3px 10px; + border-radius: 4px; + cursor: pointer; + transition: background 0.2s; + } + + #add-node-btn:hover { + background: rgba(79, 195, 247, 0.25); + } + + /* esp-web-install-button overrides */ + esp-web-install-button { + display: block; + } + + esp-web-install-button::part(button) { + background: #4fc3f7; + color: #1a1a2e; + border: none; + padding: 10px 24px; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + } @@ -407,6 +785,7 @@ CLEAR
+ @@ -467,6 +846,10 @@ + + + +