745 lines
32 KiB
HTML
745 lines
32 KiB
HTML
<!doctype html>
|
|
<meta charset="utf-8">
|
|
<title>Provisioning Wizard — Device · Bellsystems Console</title>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">
|
|
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="wizard.css">
|
|
<style>
|
|
/* Page-specific */
|
|
.mode-pill{
|
|
display:inline-flex;align-items:center;gap:8px;
|
|
padding:6px 12px;border-radius:999px;
|
|
background:var(--bg-3);border:1px solid var(--line);
|
|
font-family:var(--mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase;
|
|
color:var(--text-1);
|
|
}
|
|
.mode-pill .swatch{width:6px;height:6px;border-radius:50%;background:var(--accent);box-shadow:0 0 8px var(--accent)}
|
|
.mode-pill.new .swatch{background:var(--violet);box-shadow:0 0 8px var(--violet)}
|
|
|
|
/* Search box */
|
|
.search-wrap{
|
|
position:relative;
|
|
padding:0;
|
|
border-bottom:1px solid var(--line);
|
|
}
|
|
.search-wrap input{
|
|
width:100%;
|
|
padding:18px 20px 18px 54px;
|
|
background:transparent;border:0;
|
|
color:var(--text-0);font-family:var(--sans);font-size:15px;
|
|
outline:none;
|
|
letter-spacing:-0.005em;
|
|
}
|
|
.search-wrap input::placeholder{color:var(--text-3)}
|
|
.search-wrap .search-icon{
|
|
position:absolute;left:20px;top:50%;transform:translateY(-50%);
|
|
color:var(--text-3);pointer-events:none;
|
|
}
|
|
.search-wrap .kbd-hint{
|
|
position:absolute;right:20px;top:50%;transform:translateY(-50%);
|
|
display:flex;gap:4px;
|
|
}
|
|
.search-wrap .kbd-hint kbd{
|
|
font-family:var(--mono);font-size:10px;padding:2px 6px;border-radius:4px;
|
|
background:var(--bg-3);border:1px solid var(--line-strong);color:var(--text-2);
|
|
}
|
|
|
|
.filter-bar{
|
|
display:flex;align-items:center;gap:8px;
|
|
padding:10px 20px;
|
|
border-bottom:1px solid var(--line);
|
|
background:rgba(0,0,0,.15);
|
|
flex-wrap:wrap;
|
|
}
|
|
.chip{
|
|
display:inline-flex;align-items:center;gap:6px;
|
|
padding:4px 10px;border-radius:999px;
|
|
background:var(--bg-3);border:1px solid var(--line);
|
|
font-family:var(--mono);font-size:10.5px;letter-spacing:.05em;
|
|
color:var(--text-2);cursor:pointer;transition:.15s;
|
|
text-transform:uppercase;
|
|
}
|
|
.chip:hover{color:var(--text-0);border-color:var(--line-strong)}
|
|
.chip.active{background:rgba(34,224,122,.08);color:var(--accent);border-color:rgba(34,224,122,.3)}
|
|
.chip .count{
|
|
font-size:9.5px;padding:1px 5px;border-radius:3px;
|
|
background:var(--bg-1);color:var(--text-3);
|
|
border:1px solid var(--line-strong);
|
|
}
|
|
.chip.active .count{background:rgba(34,224,122,.15);color:var(--accent);border-color:transparent}
|
|
.filter-bar .sort{
|
|
margin-left:auto;color:var(--text-3);font-family:var(--mono);font-size:10.5px;
|
|
display:flex;align-items:center;gap:6px;letter-spacing:.05em;text-transform:uppercase;
|
|
}
|
|
|
|
/* Device rows */
|
|
.device-list{
|
|
max-height:540px;overflow:auto;
|
|
scrollbar-width:thin;scrollbar-color: var(--line-strong) transparent;
|
|
}
|
|
.device-list::-webkit-scrollbar{width:8px}
|
|
.device-list::-webkit-scrollbar-thumb{background:var(--line-strong);border-radius:4px}
|
|
|
|
.device-row{
|
|
display:grid;
|
|
grid-template-columns: 28px minmax(0,1.4fr) minmax(0,1fr) 130px 120px;
|
|
align-items:center;
|
|
gap:14px;
|
|
padding:12px 20px;
|
|
border-bottom:1px solid var(--line);
|
|
cursor:pointer;
|
|
transition:.12s;
|
|
position:relative;
|
|
}
|
|
.device-row:last-child{border-bottom:0}
|
|
.device-row:hover{background:rgba(255,255,255,.02)}
|
|
.device-row.selected{
|
|
background:rgba(34,224,122,.04);
|
|
}
|
|
.device-row.selected::before{
|
|
content:"";position:absolute;left:0;top:0;bottom:0;width:2px;
|
|
background:var(--accent);box-shadow:0 0 12px var(--accent);
|
|
}
|
|
|
|
.device-row .check{
|
|
width:18px;height:18px;border-radius:50%;
|
|
border:1.5px solid var(--line-strong);
|
|
background:var(--bg-3);
|
|
display:grid;place-items:center;color:transparent;
|
|
}
|
|
.device-row.selected .check{
|
|
background:var(--accent);border-color:transparent;color:var(--accent-ink);
|
|
}
|
|
.device-row .check svg{width:10px;height:10px}
|
|
|
|
.device-row .serial-cell{
|
|
font-family:var(--mono);font-size:13px;font-weight:500;
|
|
color:var(--text-0);
|
|
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
|
|
}
|
|
.device-row .serial-cell .meta-sub{
|
|
font-size:10.5px;color:var(--text-3);font-weight:400;margin-top:2px;
|
|
letter-spacing:.04em;text-transform:uppercase;
|
|
}
|
|
.device-row .board-cell-x{
|
|
display:flex;align-items:center;gap:8px;
|
|
font-size:13px;color:var(--text-1);
|
|
}
|
|
.device-row .board-cell-x .rev{
|
|
font-family:var(--mono);font-size:10.5px;color:var(--text-3);
|
|
padding:1px 6px;border-radius:4px;background:var(--bg-3);border:1px solid var(--line);
|
|
}
|
|
.device-row .date{
|
|
font-family:var(--mono);font-size:11px;color:var(--text-3);
|
|
}
|
|
|
|
.status-badge{
|
|
display:inline-flex;align-items:center;gap:6px;
|
|
padding:3px 9px;border-radius:999px;
|
|
font-family:var(--mono);font-size:10px;font-weight:600;
|
|
letter-spacing:.1em;text-transform:uppercase;
|
|
border:1px solid transparent;
|
|
justify-self:start;
|
|
}
|
|
.status-badge .sdot{width:5px;height:5px;border-radius:50%;background:currentColor}
|
|
.status-badge.manufactured{background:rgba(90,176,255,.08);color:var(--blue);border-color:rgba(90,176,255,.22)}
|
|
.status-badge.flashed{background:rgba(255,179,71,.08);color:var(--warn);border-color:rgba(255,179,71,.22)}
|
|
.status-badge.provisioned{background:rgba(34,224,122,.08);color:var(--accent);border-color:rgba(34,224,122,.22)}
|
|
|
|
/* -------- NEW DEVICE FORM -------- */
|
|
.new-wrap{padding:24px 24px 28px;display:flex;flex-direction:column;gap:22px}
|
|
.field-label{
|
|
font-size:10.5px;letter-spacing:.14em;color:var(--text-3);
|
|
text-transform:uppercase;font-weight:600;margin-bottom:10px;
|
|
display:flex;align-items:center;gap:8px;
|
|
}
|
|
.field-label .req{color:var(--accent);font-weight:700;letter-spacing:0}
|
|
|
|
.board-select-grid{
|
|
display:grid;grid-template-columns:repeat(4, minmax(0,1fr));gap:8px;
|
|
}
|
|
.board-select-grid .bopt{
|
|
padding:14px 12px;border:1px solid var(--line);border-radius:10px;
|
|
background:rgba(255,255,255,.012);cursor:pointer;
|
|
display:flex;flex-direction:column;gap:6px;
|
|
transition:.15s;
|
|
text-align:left;
|
|
}
|
|
.board-select-grid .bopt:hover{border-color:var(--line-strong);background:rgba(255,255,255,.03)}
|
|
.board-select-grid .bopt.selected{
|
|
border-color:rgba(34,224,122,.4);
|
|
background:rgba(34,224,122,.05);
|
|
box-shadow:inset 0 0 0 1px rgba(34,224,122,.2);
|
|
}
|
|
.board-select-grid .bopt .name{font-size:13.5px;font-weight:500;color:var(--text-0);letter-spacing:-0.005em}
|
|
.board-select-grid .bopt.selected .name{color:var(--accent)}
|
|
.board-select-grid .bopt .chip-small{
|
|
font-family:var(--mono);font-size:9.5px;color:var(--text-3);
|
|
letter-spacing:.05em;text-transform:uppercase;
|
|
}
|
|
|
|
.board-select-grid .bopt.bespoke{
|
|
border-style:dashed;
|
|
grid-column:span 2;
|
|
align-items:flex-start;
|
|
}
|
|
.board-select-grid .bopt.bespoke .name{color:var(--violet)}
|
|
.board-select-grid .bopt.bespoke:hover{border-color:rgba(180,139,255,.4);background:rgba(180,139,255,.04)}
|
|
|
|
.rev-row{
|
|
display:flex;gap:8px;flex-wrap:wrap;
|
|
}
|
|
.rev-row .rv{
|
|
padding:8px 16px;border-radius:8px;border:1px solid var(--line);
|
|
background:var(--bg-3);color:var(--text-1);cursor:pointer;
|
|
font-family:var(--mono);font-size:12px;font-weight:500;letter-spacing:.04em;
|
|
transition:.15s;
|
|
}
|
|
.rev-row .rv:hover{color:var(--text-0);border-color:var(--line-strong)}
|
|
.rev-row .rv.selected{
|
|
background:rgba(34,224,122,.08);color:var(--accent);
|
|
border-color:rgba(34,224,122,.35);
|
|
}
|
|
|
|
.serial-preview{
|
|
padding:18px 20px;
|
|
border:1px solid rgba(34,224,122,.22);
|
|
border-radius:10px;
|
|
background:
|
|
radial-gradient(400px 120px at 20% 0%, rgba(34,224,122,.06), transparent 70%),
|
|
rgba(34,224,122,.03);
|
|
display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:16px;
|
|
}
|
|
.serial-preview .psym{
|
|
width:36px;height:36px;border-radius:8px;
|
|
background:rgba(34,224,122,.1);border:1px solid rgba(34,224,122,.3);
|
|
display:grid;place-items:center;color:var(--accent);
|
|
}
|
|
.serial-preview .ptxt .k{
|
|
font-size:10.5px;letter-spacing:.14em;color:var(--text-3);
|
|
text-transform:uppercase;font-weight:600;
|
|
}
|
|
.serial-preview .ptxt .v{
|
|
font-family:var(--mono);font-size:18px;font-weight:600;
|
|
color:var(--text-0);margin-top:4px;letter-spacing:0;
|
|
}
|
|
.serial-preview .ptxt .v .muted{color:var(--text-3);font-weight:400}
|
|
.serial-preview .regen{
|
|
display:inline-flex;align-items:center;gap:6px;
|
|
padding:6px 10px;border-radius:6px;
|
|
background:var(--bg-3);border:1px solid var(--line);
|
|
color:var(--text-2);font-family:var(--mono);font-size:11px;cursor:pointer;
|
|
text-transform:uppercase;letter-spacing:.05em;
|
|
}
|
|
.serial-preview .regen:hover{color:var(--text-0);border-color:var(--line-strong)}
|
|
.serial-preview .regen svg{width:12px;height:12px}
|
|
|
|
/* ----- BESPOKE MODAL ----- */
|
|
.modal-scrim{
|
|
position:fixed;inset:0;background:rgba(0,0,0,.7);
|
|
backdrop-filter:blur(6px);
|
|
display:none;
|
|
align-items:center;justify-content:center;
|
|
z-index:80;
|
|
padding:40px 20px;
|
|
}
|
|
.modal-scrim.open{display:flex}
|
|
.modal{
|
|
width:min(720px,100%);
|
|
background:linear-gradient(180deg, var(--bg-1), var(--bg-2) 60%);
|
|
border:1px solid var(--line-strong);border-radius:14px;
|
|
box-shadow:0 40px 120px -20px rgba(0,0,0,.8);
|
|
max-height:86vh;display:flex;flex-direction:column;
|
|
overflow:hidden;
|
|
}
|
|
.modal-head{
|
|
padding:16px 22px;border-bottom:1px solid var(--line);
|
|
display:flex;align-items:center;justify-content:space-between;
|
|
}
|
|
.modal-head h3{margin:0;font-size:14px;font-weight:600;letter-spacing:-0.005em;display:flex;align-items:center;gap:10px}
|
|
.modal-head h3 .violet-dot{width:6px;height:6px;border-radius:50%;background:var(--violet);box-shadow:0 0 10px var(--violet)}
|
|
.modal-head .close{
|
|
width:28px;height:28px;border-radius:6px;
|
|
background:transparent;border:1px solid var(--line);
|
|
color:var(--text-3);cursor:pointer;display:grid;place-items:center;
|
|
}
|
|
.modal-head .close:hover{color:var(--text-0);border-color:var(--line-strong);background:var(--bg-3)}
|
|
|
|
.bespoke-list{overflow:auto;max-height:60vh}
|
|
.bespoke-item{
|
|
display:grid;grid-template-columns: 1fr auto auto;gap:16px;align-items:center;
|
|
padding:14px 22px;border-bottom:1px solid var(--line);cursor:pointer;
|
|
transition:.12s;
|
|
}
|
|
.bespoke-item:hover{background:rgba(255,255,255,.025)}
|
|
.bespoke-item .bname{font-size:13.5px;color:var(--text-0);font-weight:500}
|
|
.bespoke-item .bcode{font-family:var(--mono);font-size:11px;color:var(--text-3);margin-top:2px}
|
|
.bespoke-item .bclient{
|
|
font-family:var(--mono);font-size:11px;color:var(--text-2);
|
|
padding:3px 8px;border-radius:4px;background:var(--bg-3);border:1px solid var(--line);
|
|
}
|
|
.bespoke-item .bunits{font-family:var(--mono);font-size:10.5px;color:var(--text-3);letter-spacing:.05em;text-transform:uppercase}
|
|
|
|
/* Empty state for device-list */
|
|
.list-empty{
|
|
padding:60px 20px;text-align:center;
|
|
color:var(--text-3);font-family:var(--mono);font-size:12px;
|
|
display:flex;flex-direction:column;align-items:center;gap:12px;
|
|
}
|
|
.list-empty .bigdot{
|
|
width:48px;height:48px;border-radius:50%;
|
|
background:var(--bg-3);border:1px solid var(--line);
|
|
display:grid;place-items:center;color:var(--text-3);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="layout-split">
|
|
|
|
<div class="shell">
|
|
|
|
<!-- TOP BAR -->
|
|
<div class="topbar">
|
|
<div class="brand">
|
|
<div class="logo" aria-hidden="true">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="#22e07a" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M6 3h8l4 4v14H6z"></path>
|
|
<path d="M14 3v4h4"></path>
|
|
<path d="M10 12l-2 3h3l-1 3 3-4h-2l1-2z" fill="#22e07a" stroke="none"></path>
|
|
</svg>
|
|
</div>
|
|
<div class="title">
|
|
<h1>Provisioning Wizard</h1>
|
|
<p>Flash firmware to a Bell Systems board via WebSerial</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="top-actions" style="margin-top:2px">
|
|
<a class="btn btn-ghost" href="Mode Select.html">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5"></path><path d="M12 19l-7-7 7-7"></path></svg>
|
|
Change mode
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- STEPPER -->
|
|
<div class="stepper" role="list">
|
|
<div class="step done" role="listitem">
|
|
<div class="num">
|
|
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>
|
|
</div>
|
|
<div class="meta"><span class="label">Mode</span><span class="sub" id="step-mode-sub">existing</span></div>
|
|
<div class="step-divider"></div>
|
|
</div>
|
|
<div class="step active" role="listitem">
|
|
<div class="num">2</div>
|
|
<div class="meta"><span class="label">Device</span><span class="sub" id="step-device-sub">select</span></div>
|
|
<div class="step-divider"></div>
|
|
</div>
|
|
<div class="step" role="listitem">
|
|
<div class="num">3</div>
|
|
<div class="meta"><span class="label">Flash</span><span class="sub">pending</span></div>
|
|
<div class="step-divider"></div>
|
|
</div>
|
|
<div class="step" role="listitem">
|
|
<div class="num">4</div>
|
|
<div class="meta"><span class="label">Verify</span><span class="sub">pending</span></div>
|
|
<div class="step-divider"></div>
|
|
</div>
|
|
<div class="step" role="listitem">
|
|
<div class="num">5</div>
|
|
<div class="meta"><span class="label">Done</span><span class="sub">pending</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- EXISTING DEVICE CARD -->
|
|
<section class="card" id="existingCard" aria-label="Pick existing device">
|
|
<div class="card-head">
|
|
<h2><span class="dot"></span> Select a device from inventory</h2>
|
|
<span class="mode-pill"><span class="swatch"></span>Flash existing</span>
|
|
</div>
|
|
|
|
<!-- Search -->
|
|
<div class="search-wrap">
|
|
<svg class="search-icon" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="11" cy="11" r="7"></circle>
|
|
<path d="M21 21l-4.3-4.3"></path>
|
|
</svg>
|
|
<input id="searchInput" placeholder="Search by serial · e.g. BSVSPR-26D19J or VSPR-26" autocomplete="off" spellcheck="false">
|
|
<div class="kbd-hint"><kbd>⌘</kbd><kbd>K</kbd></div>
|
|
</div>
|
|
|
|
<!-- Filter chips -->
|
|
<div class="filter-bar">
|
|
<span class="chip active" data-filter="all">All <span class="count" id="count-all">0</span></span>
|
|
<span class="chip" data-filter="manufactured">Manufactured <span class="count" id="count-manufactured">0</span></span>
|
|
<span class="chip" data-filter="flashed">Flashed <span class="count" id="count-flashed">0</span></span>
|
|
<span class="chip" data-filter="provisioned">Provisioned <span class="count" id="count-provisioned">0</span></span>
|
|
<div class="sort">
|
|
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h13M3 12h9M3 18h5M17 18l4-4M21 18l-4-4"></path></svg>
|
|
Newest first
|
|
</div>
|
|
</div>
|
|
|
|
<!-- List -->
|
|
<div class="device-list" id="deviceList"></div>
|
|
|
|
<!-- Action bar -->
|
|
<div class="action-bar">
|
|
<div class="meta-inline">
|
|
<span class="k">SELECTED</span>
|
|
<span class="v" id="existingSelLabel" style="font-family:var(--mono)">— none —</span>
|
|
<span class="sep">/</span>
|
|
<span class="k">TOTAL ELIGIBLE</span><span class="v" id="totalEligible">0</span>
|
|
</div>
|
|
<div style="display:flex;gap:10px">
|
|
<a class="btn btn-ghost" href="Mode Select.html">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5"></path><path d="M12 19l-7-7 7-7"></path></svg>
|
|
Back
|
|
</a>
|
|
<a class="btn btn-primary btn-lg" id="existingContinue" href="Flashing.html" style="opacity:.5;pointer-events:none">
|
|
Continue to flash
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"></path><path d="M12 5l7 7-7 7"></path></svg>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- NEW DEVICE CARD -->
|
|
<section class="card hide" id="newCard" aria-label="Configure new device">
|
|
<div class="card-head">
|
|
<h2><span class="dot"></span> Configure new device</h2>
|
|
<span class="mode-pill new"><span class="swatch"></span>Deploy new</span>
|
|
</div>
|
|
|
|
<div class="new-wrap">
|
|
|
|
<!-- Board type -->
|
|
<div>
|
|
<div class="field-label">Board type <span class="req">*</span></div>
|
|
<div class="board-select-grid" id="boardGrid">
|
|
<button class="bopt" data-board="vesper"><span class="name">Vesper</span><span class="chip-small">ESP32-S3 · VSPR</span></button>
|
|
<button class="bopt" data-board="vesper-plus"><span class="name">Vesper Plus</span><span class="chip-small">ESP32-S3 · VSPP</span></button>
|
|
<button class="bopt" data-board="vesper-pro"><span class="name">Vesper Pro</span><span class="chip-small">ESP32-S3 · VSPO</span></button>
|
|
<button class="bopt" data-board="agnus-mini"><span class="name">Agnus Mini</span><span class="chip-small">ESP32-C6 · AGNM</span></button>
|
|
<button class="bopt" data-board="agnus"><span class="name">Agnus</span><span class="chip-small">ESP32-C6 · AGNU</span></button>
|
|
<button class="bopt" data-board="chronos"><span class="name">Chronos</span><span class="chip-small">nRF52 · CHRN</span></button>
|
|
<button class="bopt" data-board="chronos-pro"><span class="name">Chronos Pro</span><span class="chip-small">nRF52 · CHRP</span></button>
|
|
<button class="bopt bespoke" id="openBespoke">
|
|
<span class="name" style="display:flex;align-items:center;gap:8px">
|
|
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M20 12a8 8 0 1 1-16 0 8 8 0 0 1 16 0z"></path><path d="M12 8v4l3 2"></path></svg>
|
|
Select Bespoke…
|
|
</span>
|
|
<span class="chip-small">1-off client builds · 12 active</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Revision -->
|
|
<div>
|
|
<div class="field-label">Revision <span class="req">*</span></div>
|
|
<div class="rev-row" id="revRow">
|
|
<button class="rv" data-rev="1.0">Rev 1.0</button>
|
|
<button class="rv selected" data-rev="1.1">Rev 1.1</button>
|
|
<button class="rv" data-rev="2.0">Rev 2.0</button>
|
|
<button class="rv" data-rev="2.1">Rev 2.1</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Serial preview -->
|
|
<div class="serial-preview" id="serialPreview">
|
|
<div class="psym">
|
|
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"></path></svg>
|
|
</div>
|
|
<div class="ptxt">
|
|
<div class="k">Next serial number</div>
|
|
<div class="v" id="nextSerial"><span class="muted">Pick a board type to generate…</span></div>
|
|
</div>
|
|
<button class="regen" id="regenBtn" style="opacity:.4;pointer-events:none">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"></polyline><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path></svg>
|
|
Regenerate
|
|
</button>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Action bar -->
|
|
<div class="action-bar">
|
|
<div class="meta-inline">
|
|
<span class="k">BOARD</span><span class="v" id="newBoardLabel" style="font-family:var(--mono)">— none —</span>
|
|
<span class="sep">/</span>
|
|
<span class="k">REV</span><span class="v" id="newRevLabel" style="font-family:var(--mono)">1.1</span>
|
|
</div>
|
|
<div style="display:flex;gap:10px">
|
|
<a class="btn btn-ghost" href="Mode Select.html">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5"></path><path d="M12 19l-7-7 7-7"></path></svg>
|
|
Back
|
|
</a>
|
|
<a class="btn btn-primary btn-lg" id="newContinue" href="Flashing.html" style="opacity:.5;pointer-events:none">
|
|
Mint & continue
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"></path><path d="M12 5l7 7-7 7"></path></svg>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
</div>
|
|
|
|
<!-- Bespoke modal -->
|
|
<div class="modal-scrim" id="bespokeModal">
|
|
<div class="modal">
|
|
<div class="modal-head">
|
|
<h3><span class="violet-dot"></span>Bespoke builds</h3>
|
|
<button class="close" id="closeBespoke" aria-label="Close">
|
|
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18M6 6l12 12"></path></svg>
|
|
</button>
|
|
</div>
|
|
<div class="bespoke-list" id="bespokeList"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
/* ---------- DATA ---------- */
|
|
const BOARD_CODES = {
|
|
'vesper':'VSPR','vesper-plus':'VSPP','vesper-pro':'VSPO',
|
|
'agnus-mini':'AGNM','agnus':'AGNU','chronos':'CHRN','chronos-pro':'CHRP',
|
|
};
|
|
const BOARD_LABELS = {
|
|
'vesper':'Vesper','vesper-plus':'Vesper Plus','vesper-pro':'Vesper Pro',
|
|
'agnus-mini':'Agnus Mini','agnus':'Agnus','chronos':'Chronos','chronos-pro':'Chronos Pro',
|
|
};
|
|
|
|
// Existing devices (mock)
|
|
const DEVICES = [
|
|
{s:'BSVSPR-26D19J-STD10R-XSNBBP', board:'Vesper', code:'VSPR', rev:'1.0', status:'manufactured', date:'2026-03-18'},
|
|
{s:'BSAGNU-26D18K-STD11R-7KXQMA', board:'Agnus', code:'AGNU', rev:'1.1', status:'flashed', date:'2026-03-17'},
|
|
{s:'BSCHRN-26D17L-STD20R-P3ZWNV', board:'Chronos', code:'CHRN', rev:'2.0', status:'provisioned', date:'2026-03-16'},
|
|
{s:'BSVSPP-26D17L-STD11R-M9RLXB', board:'Vesper Plus', code:'VSPP', rev:'1.1', status:'manufactured', date:'2026-03-16'},
|
|
{s:'BSVSPO-26D15N-STD20R-QK27YH', board:'Vesper Pro', code:'VSPO', rev:'2.0', status:'flashed', date:'2026-03-14'},
|
|
{s:'BSAGNM-26D14P-STD10R-L8D4WR', board:'Agnus Mini', code:'AGNM', rev:'1.0', status:'manufactured', date:'2026-03-13'},
|
|
{s:'BSVSPR-26D14P-STD21R-HBFV6Z', board:'Vesper', code:'VSPR', rev:'2.1', status:'provisioned', date:'2026-03-13'},
|
|
{s:'BSCHRP-26D12R-STD20R-TNW1CK', board:'Chronos Pro', code:'CHRP', rev:'2.0', status:'flashed', date:'2026-03-11'},
|
|
{s:'BSVSPR-26D11S-STD11R-R5EMBX', board:'Vesper', code:'VSPR', rev:'1.1', status:'manufactured', date:'2026-03-10'},
|
|
{s:'BSAGNU-26D10T-STD11R-V2PGLY', board:'Agnus', code:'AGNU', rev:'1.1', status:'provisioned', date:'2026-03-09'},
|
|
{s:'BSVSPP-26D08V-STD20R-D7FH3S', board:'Vesper Plus', code:'VSPP', rev:'2.0', status:'flashed', date:'2026-03-07'},
|
|
{s:'BSCHRN-26D06X-STD10R-A1KYUM', board:'Chronos', code:'CHRN', rev:'1.0', status:'manufactured', date:'2026-03-05'},
|
|
{s:'BSVSPR-26D05Y-STD10R-J0QVXC', board:'Vesper', code:'VSPR', rev:'1.0', status:'provisioned', date:'2026-03-04'},
|
|
{s:'BSAGNM-26D04Z-STD11R-E6ZRTB', board:'Agnus Mini', code:'AGNM', rev:'1.1', status:'flashed', date:'2026-03-03'},
|
|
];
|
|
|
|
const BESPOKE = [
|
|
{name:'Orchid Mk II', code:'ORCH-MK2', client:'Kestrel Labs', units:'24 units'},
|
|
{name:'Lighthouse Mini', code:'LTHS-MIN', client:'Meridian Harbor Co.',units:'12 units'},
|
|
{name:'Drydock Sensor', code:'DRDK-S01', client:'Pelican Freight', units:'48 units'},
|
|
{name:'Hummingbird', code:'HMBR-V1', client:'Aviary Research', units:'8 units'},
|
|
{name:'Parchment Reader', code:'PRCH-R2', client:'Archive Society', units:'16 units'},
|
|
{name:'Tideline Logger', code:'TDLN-L1', client:'Coastal Institute', units:'32 units'},
|
|
];
|
|
|
|
/* ---------- INIT + ROUTING ---------- */
|
|
const params = new URLSearchParams(location.search);
|
|
const mode = params.get('mode') || localStorage.getItem('wizard.mode') || 'existing';
|
|
document.getElementById('step-mode-sub').textContent = mode === 'new' ? 'new' : 'existing';
|
|
if (mode === 'new'){
|
|
document.getElementById('existingCard').classList.add('hide');
|
|
document.getElementById('newCard').classList.remove('hide');
|
|
}
|
|
|
|
/* ---------- EXISTING DEVICE LIST ---------- */
|
|
const listEl = document.getElementById('deviceList');
|
|
let activeFilter = 'all';
|
|
let selectedSerial = null;
|
|
let query = '';
|
|
|
|
function renderList(){
|
|
const q = query.toLowerCase();
|
|
const filtered = DEVICES.filter(d => {
|
|
if (activeFilter !== 'all' && d.status !== activeFilter) return false;
|
|
if (q && !d.s.toLowerCase().includes(q) && !d.board.toLowerCase().includes(q)) return false;
|
|
return true;
|
|
});
|
|
|
|
// counts
|
|
document.getElementById('count-all').textContent = DEVICES.length;
|
|
['manufactured','flashed','provisioned'].forEach(s => {
|
|
document.getElementById('count-'+s).textContent = DEVICES.filter(d => d.status === s).length;
|
|
});
|
|
document.getElementById('totalEligible').textContent = filtered.length + ' / ' + DEVICES.length;
|
|
|
|
if (!filtered.length){
|
|
listEl.innerHTML = `
|
|
<div class="list-empty">
|
|
<div class="bigdot">
|
|
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"></circle><path d="M21 21l-4.3-4.3"></path></svg>
|
|
</div>
|
|
<div>No devices match the current filter.</div>
|
|
</div>`;
|
|
return;
|
|
}
|
|
|
|
listEl.innerHTML = filtered.map(d => `
|
|
<div class="device-row ${d.s === selectedSerial ? 'selected' : ''}" data-serial="${d.s}">
|
|
<div class="check">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>
|
|
</div>
|
|
<div class="serial-cell">
|
|
${d.s}
|
|
<div class="meta-sub">added ${d.date}</div>
|
|
</div>
|
|
<div class="board-cell-x">
|
|
${d.board}
|
|
<span class="rev">Rev ${d.rev}</span>
|
|
</div>
|
|
<div class="date">${d.date}</div>
|
|
<span class="status-badge ${d.status}"><span class="sdot"></span>${d.status}</span>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
listEl.addEventListener('click', e => {
|
|
const row = e.target.closest('.device-row');
|
|
if (!row) return;
|
|
selectedSerial = row.dataset.serial;
|
|
document.getElementById('existingSelLabel').textContent = selectedSerial;
|
|
const btn = document.getElementById('existingContinue');
|
|
btn.style.opacity = '1'; btn.style.pointerEvents = 'auto';
|
|
btn.href = 'Flashing.html?serial=' + encodeURIComponent(selectedSerial);
|
|
document.getElementById('step-device-sub').textContent = selectedSerial.split('-')[0].replace('BS','').toLowerCase();
|
|
renderList();
|
|
});
|
|
|
|
document.querySelectorAll('.chip[data-filter]').forEach(c => {
|
|
c.addEventListener('click', () => {
|
|
document.querySelectorAll('.chip[data-filter]').forEach(x => x.classList.remove('active'));
|
|
c.classList.add('active');
|
|
activeFilter = c.dataset.filter;
|
|
renderList();
|
|
});
|
|
});
|
|
|
|
document.getElementById('searchInput').addEventListener('input', e => {
|
|
query = e.target.value;
|
|
renderList();
|
|
});
|
|
|
|
document.addEventListener('keydown', e => {
|
|
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k'){
|
|
e.preventDefault();
|
|
document.getElementById('searchInput').focus();
|
|
}
|
|
});
|
|
|
|
renderList();
|
|
|
|
/* ---------- NEW DEVICE FORM ---------- */
|
|
let selBoard = null;
|
|
let selRev = '1.1';
|
|
|
|
function randSeg(len){
|
|
const chars='0123456789ABCDEFGHJKLMNPQRSTUVWXYZ';
|
|
let s=''; for (let i=0;i<len;i++) s += chars[Math.floor(Math.random()*chars.length)];
|
|
return s;
|
|
}
|
|
function mintSerial(boardKey, rev){
|
|
if (!boardKey) return null;
|
|
const code = BOARD_CODES[boardKey];
|
|
if (!code) return null;
|
|
const revCode = 'STD' + rev.replace('.','') + 'R';
|
|
return `BS${code}-26D${randSeg(2)}${randSeg(1)}-${revCode}-${randSeg(6)}`;
|
|
}
|
|
|
|
function renderSerial(){
|
|
const el = document.getElementById('nextSerial');
|
|
const btn = document.getElementById('newContinue');
|
|
const regen = document.getElementById('regenBtn');
|
|
if (!selBoard){
|
|
el.innerHTML = '<span class="muted">Pick a board type to generate…</span>';
|
|
btn.style.opacity = '.5'; btn.style.pointerEvents = 'none';
|
|
regen.style.opacity = '.4'; regen.style.pointerEvents = 'none';
|
|
return;
|
|
}
|
|
const s = mintSerial(selBoard, selRev);
|
|
el.textContent = s;
|
|
btn.style.opacity = '1'; btn.style.pointerEvents = 'auto';
|
|
btn.href = 'Flashing.html?serial=' + encodeURIComponent(s) + '&new=1';
|
|
regen.style.opacity = '1'; regen.style.pointerEvents = 'auto';
|
|
}
|
|
|
|
document.getElementById('boardGrid').addEventListener('click', e => {
|
|
const b = e.target.closest('.bopt');
|
|
if (!b || b.id === 'openBespoke') return;
|
|
document.querySelectorAll('#boardGrid .bopt').forEach(x => x.classList.remove('selected'));
|
|
b.classList.add('selected');
|
|
selBoard = b.dataset.board;
|
|
document.getElementById('newBoardLabel').textContent = BOARD_LABELS[selBoard] || selBoard;
|
|
document.getElementById('step-device-sub').textContent = BOARD_CODES[selBoard].toLowerCase();
|
|
renderSerial();
|
|
});
|
|
|
|
document.getElementById('revRow').addEventListener('click', e => {
|
|
const r = e.target.closest('.rv');
|
|
if (!r) return;
|
|
document.querySelectorAll('#revRow .rv').forEach(x => x.classList.remove('selected'));
|
|
r.classList.add('selected');
|
|
selRev = r.dataset.rev;
|
|
document.getElementById('newRevLabel').textContent = selRev;
|
|
renderSerial();
|
|
});
|
|
|
|
document.getElementById('regenBtn').addEventListener('click', renderSerial);
|
|
|
|
/* ---------- BESPOKE MODAL ---------- */
|
|
const scrim = document.getElementById('bespokeModal');
|
|
const bespokeList = document.getElementById('bespokeList');
|
|
bespokeList.innerHTML = BESPOKE.map(b => `
|
|
<div class="bespoke-item" data-code="${b.code}" data-name="${b.name}">
|
|
<div>
|
|
<div class="bname">${b.name}</div>
|
|
<div class="bcode">${b.code}</div>
|
|
</div>
|
|
<span class="bclient">${b.client}</span>
|
|
<span class="bunits">${b.units}</span>
|
|
</div>
|
|
`).join('');
|
|
|
|
document.getElementById('openBespoke').addEventListener('click', () => scrim.classList.add('open'));
|
|
document.getElementById('closeBespoke').addEventListener('click', () => scrim.classList.remove('open'));
|
|
scrim.addEventListener('click', e => { if (e.target === scrim) scrim.classList.remove('open'); });
|
|
|
|
bespokeList.addEventListener('click', e => {
|
|
const it = e.target.closest('.bespoke-item');
|
|
if (!it) return;
|
|
// Add a "bespoke" option dynamically
|
|
const grid = document.getElementById('boardGrid');
|
|
grid.querySelectorAll('.bopt').forEach(x => x.classList.remove('selected'));
|
|
// Inject (or update) a selected-bespoke card
|
|
let el = grid.querySelector('.bopt.bespoke-selected');
|
|
if (!el){
|
|
el = document.createElement('button');
|
|
el.className = 'bopt bespoke-selected selected';
|
|
el.innerHTML = `<span class="name" style="color:var(--violet)">${it.dataset.name}</span><span class="chip-small">Bespoke · ${it.dataset.code}</span>`;
|
|
grid.appendChild(el);
|
|
} else {
|
|
el.classList.add('selected');
|
|
el.innerHTML = `<span class="name" style="color:var(--violet)">${it.dataset.name}</span><span class="chip-small">Bespoke · ${it.dataset.code}</span>`;
|
|
}
|
|
// Use the bespoke code as-is (first 4 chars) for serial
|
|
BOARD_CODES['bespoke'] = it.dataset.code.split('-')[0].slice(0,4).toUpperCase();
|
|
BOARD_LABELS['bespoke'] = it.dataset.name + ' (bespoke)';
|
|
el.dataset.board = 'bespoke';
|
|
selBoard = 'bespoke';
|
|
document.getElementById('newBoardLabel').textContent = it.dataset.name;
|
|
document.getElementById('step-device-sub').textContent = 'bespoke';
|
|
scrim.classList.remove('open');
|
|
renderSerial();
|
|
});
|
|
|
|
document.addEventListener('keydown', e => {
|
|
if (e.key === 'Escape' && scrim.classList.contains('open')) scrim.classList.remove('open');
|
|
});
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|