Rework FRU and DMI editors: per-row inline save, all fields editable
- Replace global Save button with per-row ✓ (save) / ✗ (cancel) buttons that appear only when a field is changed - All fields shown as editable inputs; server rejects unknown fields with a clear error message instead of hiding them in the UI - Monospace font and 1.5px border for all value inputs - Server-side name→area/index lookup for fields sent without area - SAA DMI card: same per-row UX, confirm dialog kept (requires reboot) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -94,7 +94,8 @@ func fruFieldMeta(name string) (editable bool, area string, index int) {
|
||||
if e, ok := fruEditableFields[name]; ok {
|
||||
return true, e.Area, e.Index
|
||||
}
|
||||
return false, "", 0
|
||||
// All fields are shown as editable; server will reject unknown fields.
|
||||
return true, "", 0
|
||||
}
|
||||
|
||||
func (h *handler) handleAPIIPMIFRURead(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -128,7 +129,17 @@ func (h *handler) handleAPIIPMIFRUWrite(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
validAreas := map[string]bool{"c": true, "b": true, "p": true}
|
||||
for _, c := range req.Changes {
|
||||
for i, c := range req.Changes {
|
||||
if c.Area == "" {
|
||||
e, ok := fruEditableFields[c.Name]
|
||||
if !ok {
|
||||
writeError(w, http.StatusUnprocessableEntity, "field not writable via ipmitool: "+c.Name)
|
||||
return
|
||||
}
|
||||
req.Changes[i].Area = e.Area
|
||||
req.Changes[i].Index = e.Index
|
||||
c = req.Changes[i]
|
||||
}
|
||||
if !validAreas[c.Area] {
|
||||
writeError(w, http.StatusUnprocessableEntity, "invalid area: "+c.Area)
|
||||
return
|
||||
@@ -196,106 +207,78 @@ func renderIPMIFRUCard() string {
|
||||
<p style="font-size:13px;color:var(--muted);margin-bottom:12px">Reads and edits FRU fields via ipmitool (In-Band, device 0). Works on any server with IPMI support.</p>
|
||||
<div id="fru-status" style="font-size:13px;color:var(--muted);margin-bottom:8px"></div>
|
||||
<div id="fru-table"></div>
|
||||
<div id="fru-save-row" style="display:none;margin-top:12px">
|
||||
<button class="btn btn-primary" id="fru-save-btn" onclick="fruSave()">Save</button>
|
||||
<span id="fru-save-msg" style="font-size:13px;color:var(--muted);margin-left:10px"></span>
|
||||
</div>
|
||||
</div></div>
|
||||
<script>
|
||||
var fruOriginal = {};
|
||||
var _fruActBtnStyle = 'width:22px;height:22px;padding:0;font-size:13px;line-height:1;border:1px solid var(--line);border-radius:3px;background:var(--surface);cursor:pointer;vertical-align:middle;';
|
||||
var _fruInputStyle = 'width:100%;padding:3px 6px;border:1.5px solid #888;border-radius:3px;font-size:13px;font-family:monospace;background:var(--surface);color:var(--ink);';
|
||||
function fruRead() {
|
||||
document.getElementById('fru-status').textContent = 'Reading...';
|
||||
var status = document.getElementById('fru-status');
|
||||
status.textContent = 'Reading...'; status.style.color = 'var(--muted)';
|
||||
document.getElementById('fru-table').innerHTML = '';
|
||||
document.getElementById('fru-save-row').style.display = 'none';
|
||||
fetch('/api/tools/ipmi-fru', {cache:'no-store'})
|
||||
.then(function(r) {
|
||||
if (!r.ok) return r.json().then(function(e) { throw new Error(e.error || r.statusText); });
|
||||
return r.json();
|
||||
})
|
||||
.then(function(r) { return r.json().then(function(d){if(!r.ok)throw new Error(d.error||r.statusText);return d;}); })
|
||||
.then(function(fields) {
|
||||
fruOriginal = {};
|
||||
if (!fields || !fields.length) {
|
||||
document.getElementById('fru-status').textContent = 'No FRU fields returned.';
|
||||
return;
|
||||
}
|
||||
document.getElementById('fru-status').textContent = '';
|
||||
if (!fields || !fields.length) { status.textContent = 'No FRU fields returned.'; return; }
|
||||
status.textContent = '';
|
||||
var rows = fields.map(function(f) {
|
||||
var val = f.value || '';
|
||||
if (f.editable) {
|
||||
fruOriginal[f.area + '_' + f.index] = val;
|
||||
return '<tr><td style="color:var(--muted);white-space:nowrap;padding-right:16px">' + escHtml(f.name) + '</td>'
|
||||
+ '<td><input class="fru-input" style="width:100%;padding:4px 6px;border:1px solid var(--border);border-radius:3px;font-size:13px;font-family:inherit;background:var(--surface);color:var(--ink)"'
|
||||
+ ' data-area="' + escHtml(f.area) + '" data-index="' + f.index + '" data-name="' + escHtml(f.name) + '"'
|
||||
+ ' data-original="' + escHtml(val) + '" value="' + escHtml(val) + '" oninput="fruDirtyCheck()"></td></tr>';
|
||||
}
|
||||
return '<tr><td style="color:var(--muted);white-space:nowrap;padding-right:16px">' + escHtml(f.name) + '</td>'
|
||||
+ '<td style="color:var(--ink)">' + escHtml(val || '—') + '</td></tr>';
|
||||
var val = escHtml(f.value || '');
|
||||
return '<tr>'
|
||||
+ '<td style="color:var(--muted);white-space:nowrap;padding-right:16px;vertical-align:middle;font-size:13px">' + escHtml(f.name) + '</td>'
|
||||
+ '<td style="vertical-align:middle"><input class="fru-inp" style="' + _fruInputStyle + '"'
|
||||
+ ' data-area="' + escHtml(f.area||'') + '" data-index="' + (f.index||0) + '" data-name="' + escHtml(f.name) + '"'
|
||||
+ ' data-original="' + val + '" value="' + val + '" oninput="fruChanged(this)"></td>'
|
||||
+ '<td class="fru-act" style="display:none;white-space:nowrap;padding-left:6px;vertical-align:middle">'
|
||||
+ '<button style="' + _fruActBtnStyle + 'color:var(--ok-fg,green);margin-right:3px" title="Save" onclick="fruSave(this)">✓</button>'
|
||||
+ '<button style="' + _fruActBtnStyle + 'color:var(--crit-fg,#9f3a38)" title="Cancel" onclick="fruCancel(this)">✗</button>'
|
||||
+ '<span class="fru-msg" style="font-size:11px;margin-left:5px;color:var(--muted)"></span>'
|
||||
+ '</td></tr>';
|
||||
}).join('');
|
||||
document.getElementById('fru-table').innerHTML = '<table style="width:100%">' + rows + '</table>';
|
||||
fruDirtyCheck();
|
||||
document.getElementById('fru-table').innerHTML = '<table style="width:100%;border-collapse:collapse">' + rows + '</table>';
|
||||
})
|
||||
.catch(function(e) {
|
||||
document.getElementById('fru-status').textContent = 'Error: ' + e.message;
|
||||
document.getElementById('fru-status').style.color = 'var(--crit-fg)';
|
||||
});
|
||||
.catch(function(e) { status.textContent = 'Error: '+e.message; status.style.color='var(--crit-fg)'; });
|
||||
}
|
||||
function escHtml(s) {
|
||||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
return String(s==null?'':s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
function fruDirtyCheck() {
|
||||
var inputs = document.querySelectorAll('.fru-input');
|
||||
var changed = 0;
|
||||
inputs.forEach(function(el) { if (el.value !== el.dataset.original) changed++; });
|
||||
var row = document.getElementById('fru-save-row');
|
||||
var btn = document.getElementById('fru-save-btn');
|
||||
if (changed > 0) {
|
||||
row.style.display = '';
|
||||
btn.textContent = 'Save (' + changed + ' changed)';
|
||||
} else {
|
||||
row.style.display = 'none';
|
||||
}
|
||||
function fruChanged(inp) {
|
||||
inp.closest('tr').querySelector('.fru-act').style.display = inp.value !== inp.dataset.original ? '' : 'none';
|
||||
}
|
||||
function fruSave() {
|
||||
var inputs = document.querySelectorAll('.fru-input');
|
||||
var changes = [];
|
||||
inputs.forEach(function(el) {
|
||||
if (el.value !== el.dataset.original) {
|
||||
changes.push({area: el.dataset.area, index: parseInt(el.dataset.index, 10), name: el.dataset.name, value: el.value});
|
||||
}
|
||||
});
|
||||
if (!changes.length) return;
|
||||
document.getElementById('fru-save-btn').disabled = true;
|
||||
document.getElementById('fru-save-msg').textContent = 'Saving...';
|
||||
fetch('/api/tools/ipmi-fru/write', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({changes: changes})})
|
||||
.then(function(r) {
|
||||
if (!r.ok) return r.json().then(function(e) { throw new Error(e.error || r.statusText); });
|
||||
return r.json();
|
||||
})
|
||||
.then(function(d) {
|
||||
var taskId = d.task_id;
|
||||
document.getElementById('fru-save-msg').textContent = 'Task ' + taskId + ' queued…';
|
||||
var poll = setInterval(function() {
|
||||
fetch('/api/tasks', {cache:'no-store'}).then(function(r) { return r.json(); }).then(function(tasks) {
|
||||
var t = Array.isArray(tasks) ? tasks.find(function(x) { return x.id === taskId; }) : null;
|
||||
if (!t) return;
|
||||
if (t.status === 'done') {
|
||||
function fruCancel(btn) {
|
||||
var row = btn.closest('tr');
|
||||
var inp = row.querySelector('.fru-inp');
|
||||
inp.value = inp.dataset.original;
|
||||
row.querySelector('.fru-act').style.display = 'none';
|
||||
row.querySelector('.fru-msg').textContent = '';
|
||||
}
|
||||
function fruSave(btn) {
|
||||
var row = btn.closest('tr');
|
||||
var inp = row.querySelector('.fru-inp');
|
||||
var msg = row.querySelector('.fru-msg');
|
||||
var cancelBtn = row.querySelectorAll('.fru-act button')[1];
|
||||
btn.disabled = true; cancelBtn.disabled = true;
|
||||
msg.textContent = '…'; msg.style.color = 'var(--muted)';
|
||||
fetch('/api/tools/ipmi-fru/write', {method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({changes:[{area:inp.dataset.area, index:parseInt(inp.dataset.index,10), name:inp.dataset.name, value:inp.value}]})})
|
||||
.then(function(r){return r.json().then(function(d){if(!r.ok)throw new Error(d.error||r.statusText);return d;});})
|
||||
.then(function(d){
|
||||
var poll = setInterval(function(){
|
||||
fetch('/api/tasks',{cache:'no-store'}).then(function(r){return r.json();}).then(function(tasks){
|
||||
var t = Array.isArray(tasks)?tasks.find(function(x){return x.id===d.task_id;}):null;
|
||||
if(!t) return;
|
||||
if(t.status==='done'){
|
||||
clearInterval(poll);
|
||||
document.getElementById('fru-save-msg').textContent = 'Done — backup saved to fru-backups/.';
|
||||
document.getElementById('fru-save-btn').disabled = false;
|
||||
inputs.forEach(function(el) { el.dataset.original = el.value; });
|
||||
fruDirtyCheck();
|
||||
} else if (t.status === 'failed') {
|
||||
inp.dataset.original = inp.value;
|
||||
row.querySelector('.fru-act').style.display = 'none';
|
||||
msg.textContent = '';
|
||||
} else if(t.status==='failed'||t.status==='cancelled'){
|
||||
clearInterval(poll);
|
||||
document.getElementById('fru-save-msg').textContent = 'Failed: ' + (t.error || 'unknown error');
|
||||
document.getElementById('fru-save-btn').disabled = false;
|
||||
msg.textContent = t.error||t.status; msg.style.color='var(--crit-fg)';
|
||||
btn.disabled=false; cancelBtn.disabled=false;
|
||||
}
|
||||
});
|
||||
}, 1500);
|
||||
},1500);
|
||||
})
|
||||
.catch(function(e) {
|
||||
document.getElementById('fru-save-msg').textContent = 'Error: ' + e.message;
|
||||
document.getElementById('fru-save-btn').disabled = false;
|
||||
});
|
||||
.catch(function(e){ msg.textContent='Error: '+e.message; msg.style.color='var(--crit-fg)'; btn.disabled=false; cancelBtn.disabled=false; });
|
||||
}
|
||||
</script>`
|
||||
}
|
||||
|
||||
@@ -213,98 +213,85 @@ func runSAADMIWriteTask(ctx context.Context, j *jobState, exportDir string, p ta
|
||||
}
|
||||
|
||||
func renderSAADMICard() string {
|
||||
return `<div class="card"><div class="card-head">Supermicro — DMI <button class="btn btn-sm btn-secondary" onclick="saaDMIRead()" style="margin-left:auto">Read</button></div><div class="card-body">
|
||||
return `<div class="card"><div class="card-head card-head-actions">Supermicro — DMI<div class="card-head-buttons"><button class="btn btn-sm btn-secondary" onclick="saaDMIRead()">Read</button></div></div><div class="card-body">
|
||||
<p style="font-size:13px;color:var(--muted);margin-bottom:12px">Reads and edits DMI fields via SAA (In-Band).</p>
|
||||
<div id="saa-dmi-status" style="font-size:13px;color:var(--muted);margin-bottom:8px"></div>
|
||||
<div id="saa-dmi-table"></div>
|
||||
<div id="saa-dmi-save-row" style="display:none;margin-top:12px">
|
||||
<button class="btn btn-primary" id="saa-dmi-save-btn" onclick="saaDMISave()">Save</button>
|
||||
<span id="saa-dmi-save-msg" style="font-size:13px;color:var(--muted);margin-left:10px"></span>
|
||||
</div>
|
||||
</div></div>
|
||||
<script>
|
||||
function saaDMIEsc(s) {
|
||||
return String(s==null?'':s).replace(/[&<>"']/g,function(c){return{'&':'&','<':'<','>':'>','"':'"',"'":'''}[c];});
|
||||
}
|
||||
function saaDMIUpdateSaveBtn() {
|
||||
var inputs = document.querySelectorAll('#saa-dmi-table input[data-original]');
|
||||
var dirty = [];
|
||||
inputs.forEach(function(inp){if(inp.value!==inp.dataset.original)dirty.push(inp);});
|
||||
var row = document.getElementById('saa-dmi-save-row');
|
||||
var btn = document.getElementById('saa-dmi-save-btn');
|
||||
if(dirty.length>0){row.style.display='';btn.textContent='Save ('+dirty.length+' changed)';}
|
||||
else{row.style.display='none';}
|
||||
}
|
||||
var _dmiActBtnStyle = 'width:22px;height:22px;padding:0;font-size:13px;line-height:1;border:1px solid var(--line);border-radius:3px;background:var(--surface);cursor:pointer;vertical-align:middle;';
|
||||
var _dmiInputStyle = 'width:100%;padding:3px 6px;border:1.5px solid #888;border-radius:3px;font-size:13px;font-family:monospace;background:var(--surface);color:var(--ink);';
|
||||
function dmiEsc(s){return String(s==null?'':s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');}
|
||||
function saaDMIRead() {
|
||||
var status = document.getElementById('saa-dmi-status');
|
||||
var table = document.getElementById('saa-dmi-table');
|
||||
var saveRow = document.getElementById('saa-dmi-save-row');
|
||||
status.textContent = 'Reading...';
|
||||
status.style.color = 'var(--muted)';
|
||||
table.innerHTML = '';
|
||||
saveRow.style.display = 'none';
|
||||
fetch('/api/tools/saa-dmi').then(function(r){return r.json().then(function(d){if(!r.ok)throw new Error(d.error||('HTTP '+r.status));return d;});}).then(function(fields){
|
||||
status.textContent = fields.length+' field(s) loaded.';
|
||||
var rows = fields.map(function(f){
|
||||
return '<tr>'
|
||||
+'<td style="font-size:13px;white-space:nowrap;padding-right:8px">'+saaDMIEsc(f.name)+'</td>'
|
||||
+'<td style="font-family:monospace;font-size:13px;white-space:nowrap;padding-right:8px">'+saaDMIEsc(f.shn)+'</td>'
|
||||
+'<td><input type="text" value="'+saaDMIEsc(f.value)+'" data-shn="'+saaDMIEsc(f.shn)+'" data-original="'+saaDMIEsc(f.value)+'" oninput="saaDMIMarkDirty(this)" style="width:100%;font-family:monospace;font-size:13px;border:1px solid var(--line);padding:3px 6px;border-radius:3px"></td>'
|
||||
+'<td id="saa-dmi-dirty-'+saaDMIEsc(f.shn)+'" style="font-size:12px;color:var(--warn,#b45309);width:50px;padding-left:6px"></td>'
|
||||
+'</tr>';
|
||||
}).join('');
|
||||
table.innerHTML = '<table style="width:100%;border-collapse:collapse"><tr><th style="text-align:left;font-size:13px;padding-bottom:6px">Field</th><th style="text-align:left;font-size:13px;padding-bottom:6px">Shn</th><th style="text-align:left;font-size:13px;padding-bottom:6px">Value</th><th></th></tr>'+rows+'</table>';
|
||||
}).catch(function(e){
|
||||
status.textContent = 'Error: '+e.message;
|
||||
status.style.color = 'var(--crit-fg,#9f3a38)';
|
||||
});
|
||||
status.textContent = 'Reading...'; status.style.color = 'var(--muted)';
|
||||
document.getElementById('saa-dmi-table').innerHTML = '';
|
||||
fetch('/api/tools/saa-dmi', {cache:'no-store'})
|
||||
.then(function(r){return r.json().then(function(d){if(!r.ok)throw new Error(d.error||('HTTP '+r.status));return d;});})
|
||||
.then(function(fields){
|
||||
status.textContent = fields.length + ' field(s) loaded.';
|
||||
var rows = fields.map(function(f){
|
||||
var val = dmiEsc(f.value||'');
|
||||
return '<tr>'
|
||||
+ '<td style="font-size:13px;color:var(--muted);white-space:nowrap;padding-right:8px;vertical-align:middle">'+dmiEsc(f.name)+'</td>'
|
||||
+ '<td style="font-family:monospace;font-size:12px;color:var(--muted);white-space:nowrap;padding-right:8px;vertical-align:middle">'+dmiEsc(f.shn)+'</td>'
|
||||
+ '<td style="vertical-align:middle"><input class="dmi-inp" type="text" style="'+_dmiInputStyle+'"'
|
||||
+ ' data-shn="'+dmiEsc(f.shn)+'" data-original="'+val+'" value="'+val+'" oninput="dmiChanged(this)"></td>'
|
||||
+ '<td class="dmi-act" style="display:none;white-space:nowrap;padding-left:6px;vertical-align:middle">'
|
||||
+ '<button style="'+_dmiActBtnStyle+'color:var(--ok-fg,green);margin-right:3px" title="Save" onclick="dmiSave(this)">✓</button>'
|
||||
+ '<button style="'+_dmiActBtnStyle+'color:var(--crit-fg,#9f3a38)" title="Cancel" onclick="dmiCancel(this)">✗</button>'
|
||||
+ '<span class="dmi-msg" style="font-size:11px;margin-left:5px;color:var(--muted)"></span>'
|
||||
+ '</td></tr>';
|
||||
}).join('');
|
||||
document.getElementById('saa-dmi-table').innerHTML =
|
||||
'<table style="width:100%;border-collapse:collapse">'
|
||||
+ '<tr><th style="text-align:left;font-size:12px;color:var(--muted);padding-bottom:6px;font-weight:normal">Field</th>'
|
||||
+ '<th style="text-align:left;font-size:12px;color:var(--muted);padding-bottom:6px;font-weight:normal">SHN</th>'
|
||||
+ '<th style="text-align:left;font-size:12px;color:var(--muted);padding-bottom:6px;font-weight:normal">Value</th><th></th></tr>'
|
||||
+ rows + '</table>';
|
||||
})
|
||||
.catch(function(e){ status.textContent='Error: '+e.message; status.style.color='var(--crit-fg,#9f3a38)'; });
|
||||
}
|
||||
function saaDMIMarkDirty(inp) {
|
||||
var shn = inp.dataset.shn;
|
||||
var cell = document.getElementById('saa-dmi-dirty-'+shn);
|
||||
if(cell)cell.textContent = inp.value!==inp.dataset.original?'changed':'';
|
||||
saaDMIUpdateSaveBtn();
|
||||
function dmiChanged(inp) {
|
||||
inp.closest('tr').querySelector('.dmi-act').style.display = inp.value !== inp.dataset.original ? '' : 'none';
|
||||
}
|
||||
function saaDMIWaitTask(taskID) {
|
||||
var msg = document.getElementById('saa-dmi-save-msg');
|
||||
msg.textContent = 'Task '+taskID+' queued...';
|
||||
msg.style.color = 'var(--muted)';
|
||||
var timer = setInterval(function(){
|
||||
fetch('/api/tasks').then(function(r){return r.json();}).then(function(tasks){
|
||||
var task = (tasks||[]).find(function(t){return t.id===taskID;});
|
||||
if(!task)return;
|
||||
if(task.status==='done'||task.status==='failed'||task.status==='cancelled'){
|
||||
clearInterval(timer);
|
||||
msg.textContent = task.status==='done'?'Saved. Reboot to apply.':'Failed: '+(task.error||task.status);
|
||||
msg.style.color = task.status==='done'?'var(--ok,green)':'var(--crit-fg,#9f3a38)';
|
||||
document.getElementById('saa-dmi-save-btn').disabled = false;
|
||||
}
|
||||
}).catch(function(){});
|
||||
}, 1500);
|
||||
function dmiCancel(btn) {
|
||||
var row = btn.closest('tr');
|
||||
var inp = row.querySelector('.dmi-inp');
|
||||
inp.value = inp.dataset.original;
|
||||
row.querySelector('.dmi-act').style.display = 'none';
|
||||
row.querySelector('.dmi-msg').textContent = '';
|
||||
}
|
||||
function saaDMISave() {
|
||||
var inputs = document.querySelectorAll('#saa-dmi-table input[data-original]');
|
||||
var changes = [];
|
||||
inputs.forEach(function(inp){if(inp.value!==inp.dataset.original)changes.push({shn:inp.dataset.shn,value:inp.value});});
|
||||
if(!changes.length)return;
|
||||
var names = changes.map(function(c){return c.shn;}).join(', ');
|
||||
if(!window.confirm('Apply DMI changes for: '+names+'?\n\nThe server will need to be rebooted for changes to take effect.'))return;
|
||||
var btn = document.getElementById('saa-dmi-save-btn');
|
||||
var msg = document.getElementById('saa-dmi-save-msg');
|
||||
btn.disabled = true;
|
||||
msg.textContent = 'Submitting...';
|
||||
msg.style.color = 'var(--muted)';
|
||||
fetch('/api/tools/saa-dmi/write',{
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({changes:changes})
|
||||
}).then(function(r){return r.json().then(function(d){if(!r.ok)throw new Error(d.error||('HTTP '+r.status));return d;});}).then(function(d){
|
||||
saaDMIWaitTask(d.task_id);
|
||||
}).catch(function(e){
|
||||
msg.textContent = 'Error: '+e.message;
|
||||
msg.style.color = 'var(--crit-fg,#9f3a38)';
|
||||
btn.disabled = false;
|
||||
});
|
||||
function dmiSave(btn) {
|
||||
var row = btn.closest('tr');
|
||||
var inp = row.querySelector('.dmi-inp');
|
||||
var msg = row.querySelector('.dmi-msg');
|
||||
var cancelBtn = row.querySelectorAll('.dmi-act button')[1];
|
||||
if(!window.confirm('Apply DMI change for '+inp.dataset.shn+'?\nServer will need to reboot for changes to take effect.'))return;
|
||||
btn.disabled=true; cancelBtn.disabled=true;
|
||||
msg.textContent='…'; msg.style.color='var(--muted)';
|
||||
fetch('/api/tools/saa-dmi/write',{method:'POST',headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({changes:[{shn:inp.dataset.shn,value:inp.value}]})})
|
||||
.then(function(r){return r.json().then(function(d){if(!r.ok)throw new Error(d.error||('HTTP '+r.status));return d;});})
|
||||
.then(function(d){
|
||||
var poll=setInterval(function(){
|
||||
fetch('/api/tasks',{cache:'no-store'}).then(function(r){return r.json();}).then(function(tasks){
|
||||
var t=(tasks||[]).find(function(x){return x.id===d.task_id;});
|
||||
if(!t)return;
|
||||
if(t.status==='done'){
|
||||
clearInterval(poll);
|
||||
inp.dataset.original=inp.value;
|
||||
row.querySelector('.dmi-act').style.display='none';
|
||||
msg.textContent='Saved. Reboot to apply.'; msg.style.color='var(--ok-fg,green)';
|
||||
} else if(t.status==='failed'||t.status==='cancelled'){
|
||||
clearInterval(poll);
|
||||
msg.textContent=t.error||t.status; msg.style.color='var(--crit-fg)';
|
||||
btn.disabled=false; cancelBtn.disabled=false;
|
||||
}
|
||||
});
|
||||
},1500);
|
||||
})
|
||||
.catch(function(e){msg.textContent='Error: '+e.message; msg.style.color='var(--crit-fg)'; btn.disabled=false; cancelBtn.disabled=false;});
|
||||
}
|
||||
</script>
|
||||
</div></div>`
|
||||
</script>`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user