Every GPU-selection picker (Load/SAT, Burn, Benchmark) now renders the
card serial: "GPU N — <model> · <mem> MiB · sn: <serial>". The serial is
monospace and the digits that differ across the listed GPUs (common
prefix/suffix stripped) are emphasised so operators can tell cards apart.
Row markup was duplicated across three pages (and twice within
page_validate.go). Consolidated into a single module,
internal/webui/gpu_picker.go: beeGpuPicker.render({...}) builds every row;
gpuPickerCSS/gpuPickerJS are injected once by layoutHead/renderPage. Pages
keep their own selection-note text and multi-GPU toggles but no longer
hand-build <label> markup.
ListNvidiaGPUs() adds the serial via nvidia-smi --query-gpu=...,serial;
N/A is normalised to empty. Serial flows to the client unchanged through
the existing /api/gpu/nvidia JSON response.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AudE3Q2nd9kxxVuxPKcTho
72 lines
3.3 KiB
Go
72 lines
3.3 KiB
Go
package webui
|
|
|
|
// Shared NVIDIA GPU selection picker.
|
|
//
|
|
// Every page that lets the operator pick GPUs (Load, Burn, Benchmark) renders
|
|
// the same row markup: "GPU N — <model> · <memory> MiB · sn: <serial>". That
|
|
// markup lives here once. Pages keep their own selection-note text, multi-GPU
|
|
// mode toggles and CSS-class prefixes, but call beeGpuPicker.render({...}) from
|
|
// their *RenderGPUList wrapper instead of hand-building each <label>.
|
|
//
|
|
// The serial number is rendered monospace, with the digits that differ across
|
|
// the listed GPUs (usually a run at the end, sometimes the middle or start)
|
|
// emphasised so operators can tell cards apart at a glance.
|
|
//
|
|
// gpuPickerCSS is injected once by layoutHead; gpuPickerJS once by renderPage.
|
|
|
|
const gpuPickerCSS = `.bee-gpu-row{display:flex;align-items:flex-start;gap:8px;padding:6px 0;cursor:pointer;font-size:13px}
|
|
.bee-gpu-row input[type=checkbox]{width:16px;height:16px;margin-top:2px;flex-shrink:0}
|
|
.bee-gpu-sn{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px}
|
|
.bee-gpu-sn b{font-weight:700;color:var(--accent)}`
|
|
|
|
const gpuPickerJS = `window.beeGpuPicker={
|
|
_esc:function(s){return String(s).replace(/[&<>"]/g,function(c){return {'&':'&','<':'<','>':'>','"':'"'}[c];});},
|
|
_commonPrefix:function(arr){
|
|
arr=arr.filter(Boolean);
|
|
if(arr.length<2)return 0;
|
|
var n=0,a=arr[0];
|
|
while(n<a.length&&arr.every(function(s){return s.charAt(n)===a.charAt(n);}))n++;
|
|
return n;
|
|
},
|
|
_commonSuffix:function(arr,capLeft){
|
|
arr=arr.filter(Boolean);
|
|
if(arr.length<2)return 0;
|
|
var minLen=Math.min.apply(null,arr.map(function(s){return s.length;}));
|
|
var n=0,a=arr[0];
|
|
while(n<a.length&&capLeft+n<minLen&&arr.every(function(s){return s.charAt(s.length-1-n)===a.charAt(a.length-1-n);}))n++;
|
|
return n;
|
|
},
|
|
_fmtSerial:function(s,p,q){
|
|
s=String(s);
|
|
if(p+q>=s.length){p=0;q=0;}
|
|
if(p===0&&q===0)return this._esc(s); // nothing distinguishing to highlight
|
|
return this._esc(s.slice(0,p))+'<b>'+this._esc(s.slice(p,s.length-q))+'</b>'+this._esc(s.slice(s.length-q));
|
|
},
|
|
row:function(gpu,checkboxClass,onToggle,prefixLen,suffixLen){
|
|
var mem=gpu.memory_mb>0?' · '+gpu.memory_mb+' MiB':'';
|
|
var name=this._esc(gpu.name||('GPU '+gpu.index));
|
|
var sn=gpu.serial?' · <span class="bee-gpu-sn">sn: '+this._fmtSerial(gpu.serial,prefixLen||0,suffixLen||0)+'</span>':'';
|
|
return '<label class="bee-gpu-row">'
|
|
+'<input class="'+checkboxClass+'" type="checkbox" value="'+gpu.index+'" checked'
|
|
+(onToggle?' onchange="'+onToggle+'"':'')+'>'
|
|
+'<span><strong>GPU '+gpu.index+'</strong> — '+name+mem+sn+'</span>'
|
|
+'</label>';
|
|
},
|
|
render:function(opts){
|
|
var root=document.getElementById(opts.rootId);
|
|
if(!root)return;
|
|
var gpus=opts.gpus||[];
|
|
if(!gpus.length){
|
|
root.innerHTML=opts.emptyHTML||'<p style="color:var(--muted);font-size:13px">No NVIDIA GPUs detected.</p>';
|
|
if(opts.after)opts.after(0);
|
|
return;
|
|
}
|
|
var serials=gpus.map(function(g){return g.serial||'';});
|
|
var p=this._commonPrefix(serials);
|
|
var q=this._commonSuffix(serials,p);
|
|
var self=this;
|
|
root.innerHTML=gpus.map(function(g){return self.row(g,opts.checkboxClass,opts.onToggle,p,q);}).join('');
|
|
if(opts.after)opts.after(gpus.length);
|
|
}
|
|
};`
|