spaxel/dashboard/css/wizard.css
jedarden 03fd4e2752 feat(onboarding): migration window, Unpaired badge, re-provision flow
Nodes that connect without a valid token during the migration window are
now accepted and flagged as Unpaired rather than rejected.  Fleet health
surfaces the flag so the dashboard can show an amber Unpaired badge in
the MAC column, an "Unpaired" status badge, and a ↺ re-provision action
button that re-opens the onboarding wizard in reprove mode (skips the
firmware-flash step, targets the specific node's MAC during detect).

- ingestion/server: migrationDeadline + Unpaired flag on NodeConnection
- fleet/fleethandler: UnpairedProvider interface, merges unpaired MACs
  into fleet health response
- config: SPAXEL_MIGRATION_WINDOW_HOURS (default 24 h, range 0-168)
- main: wires migration deadline and unpaired provider at startup
- onboard.js: reprove(mac) public API, skip-flash + targeted-detect mode
- fleet.js: Unpaired badge, Re-provision button, unpaired banner, ⚠ in
  role list, reproveNode public API
- wizard.css: .wizard-reprove-banner amber styling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 18:46:47 -04:00

267 lines
5.5 KiB
CSS

/* ───────────────────────────────────────────────────────────────
Onboarding Wizard — inline-to-external extraction from simple.html
─────────────────────────────────────────────────────────────── */
#wizard-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--overlay-strong);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
#wizard-card {
background: var(--bg-card);
border-radius: var(--radius-modal);
padding: var(--space-6) var(--space-8) var(--space-5);
max-width: 560px;
width: 92%;
max-height: 90vh;
overflow-y: auto;
box-shadow: var(--shadow-xl);
}
#wizard-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-5);
}
#wizard-header h1 {
font-size: var(--text-lg);
font-weight: var(--fw-heading);
color: var(--text-primary);
}
.wizard-close {
background: none;
border: none;
color: var(--text-muted);
font-size: var(--text-2xl);
cursor: pointer;
padding: 0 var(--space-1);
line-height: 1;
}
.wizard-close:hover {
color: var(--text-primary);
}
#wizard-steps {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: var(--space-6);
gap: 0;
}
.wizard-step-dot {
width: var(--space-6);
height: var(--space-6);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: var(--text-xs);
font-weight: var(--fw-heading);
background: var(--bg-active);
color: var(--slate-7);
transition: all var(--transition-base);
flex-shrink: 0;
}
.wizard-step-dot.active {
background: var(--color-primary);
color: var(--text-on-accent);
box-shadow: 0 0 var(--space-3) var(--blue-border);
}
.wizard-step-dot.completed {
background: var(--ok);
color: var(--text-on-accent);
}
.wizard-step-line {
width: var(--space-5);
height: 2px;
background: var(--bg-active);
flex-shrink: 0;
}
.wizard-step-line.completed {
background: var(--ok);
}
#wizard-content {
min-height: 180px;
}
.wizard-step-content {
text-align: center;
}
.wizard-step-content h2 {
font-size: var(--text-lg);
margin-bottom: var(--space-2);
color: var(--text-primary);
}
.wizard-step-content p {
color: var(--text-secondary);
font-size: var(--text-sm);
line-height: var(--lh-body);
margin-bottom: var(--space-3);
}
.wizard-muted {
color: var(--text-muted) !important;
font-size: var(--text-xs) !important;
}
.wizard-center-msg {
padding: var(--space-6) 0;
text-align: center;
}
.wizard-center-msg p {
color: var(--text-muted);
margin-top: var(--space-3);
}
.wizard-error {
color: var(--alert);
font-size: var(--text-sm);
padding: var(--space-3);
background: var(--alert-bg);
border-radius: var(--radius-control);
text-align: left;
margin-top: var(--space-2);
}
.wizard-success {
color: var(--ok);
font-size: var(--text-sm);
font-weight: 500;
}
.wizard-reprove-banner {
background: rgba(251, 191, 36, 0.12);
border: 1px solid rgba(251, 191, 36, 0.4);
border-radius: var(--radius-control);
color: #fbbf24;
font-size: var(--text-sm);
padding: var(--space-3);
margin-bottom: var(--space-4);
}
.wizard-icon-large {
font-size: var(--text-2xl);
margin-bottom: var(--space-3);
}
.wizard-list {
text-align: left;
color: var(--text-secondary);
font-size: var(--text-sm);
line-height: 1.8;
margin: var(--space-3) 0;
padding-left: var(--space-5);
}
.wizard-spinner {
display: inline-block;
width: var(--space-8);
height: var(--space-8);
border: 3px solid var(--blue-border);
border-top-color: var(--color-primary);
border-radius: 50%;
animation: wizard-spin .8s linear infinite;
}
.spinner {
display: inline-block;
width: var(--space-8);
height: var(--space-8);
border: 3px solid var(--blue-border);
border-top-color: var(--color-primary);
border-radius: 50%;
animation: wizard-spin .8s linear infinite;
}
@keyframes wizard-spin {
to { transform: rotate(360deg); }
}
.wizard-progress {
margin: var(--space-4) 0;
}
.progress-bar {
width: 100%;
height: var(--space-2);
background: var(--bg-active);
border-radius: var(--space-1);
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--blue-9), var(--blue-10));
border-radius: var(--space-1);
transition: width var(--transition-base);
width: 0%;
}
.wizard-progress p {
font-size: var(--text-xs);
color: var(--text-muted);
margin-top: var(--space-2);
text-align: center;
}
.wizard-btn {
padding: var(--space-3) var(--space-6);
border-radius: var(--radius-control);
font-size: var(--text-sm);
font-weight: 500;
cursor: pointer;
border: none;
transition: background var(--transition-base), opacity var(--transition-base);
}
.wizard-btn:disabled {
opacity: .5;
cursor: not-allowed;
}
.wizard-btn-primary {
background: var(--color-primary);
color: var(--slate-1);
}
.wizard-btn-primary:hover:not(:disabled) {
background: var(--color-primary-hover);
}
.wizard-btn-secondary {
background: var(--bg-hover);
color: var(--text-secondary);
border: 1px solid var(--border-strong);
}
.wizard-btn-secondary:hover:not(:disabled) {
background: var(--bg-active);
}
#wizard-nav {
display: flex;
justify-content: space-between;
margin-top: var(--space-5);
gap: var(--space-3);
}