Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf29131116 | ||
|
|
13e6324853 | ||
|
|
892ef6fb7d |
@@ -1292,6 +1292,22 @@ func (h *handler) handleAPIInstallToRAM(w http.ResponseWriter, r *http.Request)
|
|||||||
_ = json.NewEncoder(w).Encode(map[string]string{"task_id": t.ID})
|
_ = json.NewEncoder(w).Encode(map[string]string{"task_id": t.ID})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *handler) handleAPISystemReboot(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := exec.Command("systemctl", "reboot").Start(); err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "reboot failed: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, map[string]string{"status": "rebooting"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) handleAPISystemShutdown(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := exec.Command("systemctl", "poweroff").Start(); err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "shutdown failed: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, map[string]string{"status": "shutting down"})
|
||||||
|
}
|
||||||
|
|
||||||
// ── Tools ─────────────────────────────────────────────────────────────────────
|
// ── Tools ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
var standardTools = []string{
|
var standardTools = []string{
|
||||||
|
|||||||
@@ -33,18 +33,26 @@ var fruEditableFields = map[string]struct {
|
|||||||
Area string
|
Area string
|
||||||
Index int
|
Index int
|
||||||
}{
|
}{
|
||||||
"Chassis Part Number": {"c", 0},
|
// Chassis — vendor doc names and ipmitool abbreviated names
|
||||||
|
"Chassis Part Number": {"c", 0},
|
||||||
"Chassis Serial Number": {"c", 1},
|
"Chassis Serial Number": {"c", 1},
|
||||||
"Chassis Extra": {"c", 2},
|
"Chassis Serial": {"c", 1},
|
||||||
"Board Manufacturer": {"b", 0},
|
"Chassis Extra": {"c", 2},
|
||||||
"Board Product Name": {"b", 1},
|
// Board — vendor doc names and ipmitool abbreviated names
|
||||||
"Board Serial Number": {"b", 2},
|
"Board Manufacturer": {"b", 0},
|
||||||
"Board Part Number": {"b", 3},
|
"Board Mfg": {"b", 0},
|
||||||
|
"Board Product Name": {"b", 1},
|
||||||
|
"Board Product": {"b", 1},
|
||||||
|
"Board Serial Number": {"b", 2},
|
||||||
|
"Board Serial": {"b", 2},
|
||||||
|
"Board Part Number": {"b", 3},
|
||||||
|
// Product — vendor doc names and ipmitool abbreviated names
|
||||||
"Product Manufacturer": {"p", 0},
|
"Product Manufacturer": {"p", 0},
|
||||||
"Product Name": {"p", 1},
|
"Product Name": {"p", 1},
|
||||||
"Product Part Number": {"p", 2},
|
"Product Part Number": {"p", 2},
|
||||||
"Product Version": {"p", 3},
|
"Product Version": {"p", 3},
|
||||||
"Product Serial Number": {"p", 4},
|
"Product Serial Number": {"p", 4},
|
||||||
|
"Product Serial": {"p", 4},
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseFRUOutput(output string) []fruField {
|
func parseFRUOutput(output string) []fruField {
|
||||||
@@ -86,7 +94,8 @@ func fruFieldMeta(name string) (editable bool, area string, index int) {
|
|||||||
if e, ok := fruEditableFields[name]; ok {
|
if e, ok := fruEditableFields[name]; ok {
|
||||||
return true, e.Area, e.Index
|
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) {
|
func (h *handler) handleAPIIPMIFRURead(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -120,7 +129,17 @@ func (h *handler) handleAPIIPMIFRUWrite(w http.ResponseWriter, r *http.Request)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
validAreas := map[string]bool{"c": true, "b": true, "p": true}
|
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] {
|
if !validAreas[c.Area] {
|
||||||
writeError(w, http.StatusUnprocessableEntity, "invalid area: "+c.Area)
|
writeError(w, http.StatusUnprocessableEntity, "invalid area: "+c.Area)
|
||||||
return
|
return
|
||||||
@@ -188,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>
|
<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-status" style="font-size:13px;color:var(--muted);margin-bottom:8px"></div>
|
||||||
<div id="fru-table"></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>
|
</div></div>
|
||||||
<script>
|
<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() {
|
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-table').innerHTML = '';
|
||||||
document.getElementById('fru-save-row').style.display = 'none';
|
|
||||||
fetch('/api/tools/ipmi-fru', {cache:'no-store'})
|
fetch('/api/tools/ipmi-fru', {cache:'no-store'})
|
||||||
.then(function(r) {
|
.then(function(r) { return r.json().then(function(d){if(!r.ok)throw new Error(d.error||r.statusText);return d;}); })
|
||||||
if (!r.ok) return r.json().then(function(e) { throw new Error(e.error || r.statusText); });
|
|
||||||
return r.json();
|
|
||||||
})
|
|
||||||
.then(function(fields) {
|
.then(function(fields) {
|
||||||
fruOriginal = {};
|
if (!fields || !fields.length) { status.textContent = 'No FRU fields returned.'; return; }
|
||||||
if (!fields || !fields.length) {
|
status.textContent = '';
|
||||||
document.getElementById('fru-status').textContent = 'No FRU fields returned.';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
document.getElementById('fru-status').textContent = '';
|
|
||||||
var rows = fields.map(function(f) {
|
var rows = fields.map(function(f) {
|
||||||
var val = f.value || '';
|
var val = escHtml(f.value || '');
|
||||||
if (f.editable) {
|
return '<tr>'
|
||||||
fruOriginal[f.area + '_' + f.index] = val;
|
+ '<td style="color:var(--muted);white-space:nowrap;padding-right:16px;vertical-align:middle;font-size:13px">' + escHtml(f.name) + '</td>'
|
||||||
return '<tr><td style="color:var(--muted);white-space:nowrap;padding-right:16px">' + escHtml(f.name) + '</td>'
|
+ '<td style="vertical-align:middle"><input class="fru-inp" style="' + _fruInputStyle + '"'
|
||||||
+ '<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||0) + '" data-name="' + escHtml(f.name) + '"'
|
||||||
+ ' data-area="' + escHtml(f.area) + '" data-index="' + f.index + '" data-name="' + escHtml(f.name) + '"'
|
+ ' data-original="' + val + '" value="' + val + '" oninput="fruChanged(this)"></td>'
|
||||||
+ ' data-original="' + escHtml(val) + '" value="' + escHtml(val) + '" oninput="fruDirtyCheck()"></td></tr>';
|
+ '<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>'
|
||||||
return '<tr><td style="color:var(--muted);white-space:nowrap;padding-right:16px">' + escHtml(f.name) + '</td>'
|
+ '<button style="' + _fruActBtnStyle + 'color:var(--crit-fg,#9f3a38)" title="Cancel" onclick="fruCancel(this)">✗</button>'
|
||||||
+ '<td style="color:var(--ink)">' + escHtml(val || '—') + '</td></tr>';
|
+ '<span class="fru-msg" style="font-size:11px;margin-left:5px;color:var(--muted)"></span>'
|
||||||
|
+ '</td></tr>';
|
||||||
}).join('');
|
}).join('');
|
||||||
document.getElementById('fru-table').innerHTML = '<table style="width:100%">' + rows + '</table>';
|
document.getElementById('fru-table').innerHTML = '<table style="width:100%;border-collapse:collapse">' + rows + '</table>';
|
||||||
fruDirtyCheck();
|
|
||||||
})
|
})
|
||||||
.catch(function(e) {
|
.catch(function(e) { status.textContent = 'Error: '+e.message; status.style.color='var(--crit-fg)'; });
|
||||||
document.getElementById('fru-status').textContent = 'Error: ' + e.message;
|
|
||||||
document.getElementById('fru-status').style.color = 'var(--crit-fg)';
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
function escHtml(s) {
|
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() {
|
function fruChanged(inp) {
|
||||||
var inputs = document.querySelectorAll('.fru-input');
|
inp.closest('tr').querySelector('.fru-act').style.display = inp.value !== inp.dataset.original ? '' : 'none';
|
||||||
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 fruSave() {
|
function fruCancel(btn) {
|
||||||
var inputs = document.querySelectorAll('.fru-input');
|
var row = btn.closest('tr');
|
||||||
var changes = [];
|
var inp = row.querySelector('.fru-inp');
|
||||||
inputs.forEach(function(el) {
|
inp.value = inp.dataset.original;
|
||||||
if (el.value !== el.dataset.original) {
|
row.querySelector('.fru-act').style.display = 'none';
|
||||||
changes.push({area: el.dataset.area, index: parseInt(el.dataset.index, 10), name: el.dataset.name, value: el.value});
|
row.querySelector('.fru-msg').textContent = '';
|
||||||
}
|
}
|
||||||
});
|
function fruSave(btn) {
|
||||||
if (!changes.length) return;
|
var row = btn.closest('tr');
|
||||||
document.getElementById('fru-save-btn').disabled = true;
|
var inp = row.querySelector('.fru-inp');
|
||||||
document.getElementById('fru-save-msg').textContent = 'Saving...';
|
var msg = row.querySelector('.fru-msg');
|
||||||
fetch('/api/tools/ipmi-fru/write', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({changes: changes})})
|
var cancelBtn = row.querySelectorAll('.fru-act button')[1];
|
||||||
.then(function(r) {
|
btn.disabled = true; cancelBtn.disabled = true;
|
||||||
if (!r.ok) return r.json().then(function(e) { throw new Error(e.error || r.statusText); });
|
msg.textContent = '…'; msg.style.color = 'var(--muted)';
|
||||||
return r.json();
|
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(d) {
|
.then(function(r){return r.json().then(function(d){if(!r.ok)throw new Error(d.error||r.statusText);return d;});})
|
||||||
var taskId = d.task_id;
|
.then(function(d){
|
||||||
document.getElementById('fru-save-msg').textContent = 'Task ' + taskId + ' queued…';
|
var poll = setInterval(function(){
|
||||||
var poll = setInterval(function() {
|
fetch('/api/tasks',{cache:'no-store'}).then(function(r){return r.json();}).then(function(tasks){
|
||||||
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;
|
||||||
var t = Array.isArray(tasks) ? tasks.find(function(x) { return x.id === taskId; }) : null;
|
if(!t) return;
|
||||||
if (!t) return;
|
if(t.status==='done'){
|
||||||
if (t.status === 'done') {
|
|
||||||
clearInterval(poll);
|
clearInterval(poll);
|
||||||
document.getElementById('fru-save-msg').textContent = 'Done — backup saved to fru-backups/.';
|
inp.dataset.original = inp.value;
|
||||||
document.getElementById('fru-save-btn').disabled = false;
|
row.querySelector('.fru-act').style.display = 'none';
|
||||||
inputs.forEach(function(el) { el.dataset.original = el.value; });
|
msg.textContent = '';
|
||||||
fruDirtyCheck();
|
} else if(t.status==='failed'||t.status==='cancelled'){
|
||||||
} else if (t.status === 'failed') {
|
|
||||||
clearInterval(poll);
|
clearInterval(poll);
|
||||||
document.getElementById('fru-save-msg').textContent = 'Failed: ' + (t.error || 'unknown error');
|
msg.textContent = t.error||t.status; msg.style.color='var(--crit-fg)';
|
||||||
document.getElementById('fru-save-btn').disabled = false;
|
btn.disabled=false; cancelBtn.disabled=false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, 1500);
|
},1500);
|
||||||
})
|
})
|
||||||
.catch(function(e) {
|
.catch(function(e){ msg.textContent='Error: '+e.message; msg.style.color='var(--crit-fg)'; btn.disabled=false; cancelBtn.disabled=false; });
|
||||||
document.getElementById('fru-save-msg').textContent = 'Error: ' + e.message;
|
|
||||||
document.getElementById('fru-save-btn').disabled = false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
</script>`
|
</script>`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,5 +88,28 @@ checkTools();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-head">Power</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div style="display:flex;gap:8px;align-items:center">
|
||||||
|
<button class="btn btn-secondary btn-sm" onclick="systemPower('reboot')">Reboot</button>
|
||||||
|
<button class="btn btn-secondary btn-sm" onclick="systemPower('shutdown')">Shutdown</button>
|
||||||
|
<span id="power-status" style="font-size:12px;color:var(--muted)"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function systemPower(action) {
|
||||||
|
var label = action === 'reboot' ? 'reboot' : 'shut down';
|
||||||
|
if (!confirm('Are you sure you want to ' + label + ' the server?')) return;
|
||||||
|
var el = document.getElementById('power-status');
|
||||||
|
if (el) el.textContent = action === 'reboot' ? 'Rebooting...' : 'Shutting down...';
|
||||||
|
fetch('/api/system/' + action, {method: 'POST'})
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.catch(function(e) { if (el) el.textContent = 'Error: ' + e.message; });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -213,98 +213,85 @@ func runSAADMIWriteTask(ctx context.Context, j *jobState, exportDir string, p ta
|
|||||||
}
|
}
|
||||||
|
|
||||||
func renderSAADMICard() string {
|
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>
|
<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-status" style="font-size:13px;color:var(--muted);margin-bottom:8px"></div>
|
||||||
<div id="saa-dmi-table"></div>
|
<div id="saa-dmi-table"></div>
|
||||||
<div id="saa-dmi-save-row" style="display:none;margin-top:12px">
|
</div></div>
|
||||||
<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>
|
|
||||||
<script>
|
<script>
|
||||||
function saaDMIEsc(s) {
|
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;';
|
||||||
return String(s==null?'':s).replace(/[&<>"']/g,function(c){return{'&':'&','<':'<','>':'>','"':'"',"'":'''}[c];});
|
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 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';}
|
|
||||||
}
|
|
||||||
function saaDMIRead() {
|
function saaDMIRead() {
|
||||||
var status = document.getElementById('saa-dmi-status');
|
var status = document.getElementById('saa-dmi-status');
|
||||||
var table = document.getElementById('saa-dmi-table');
|
status.textContent = 'Reading...'; status.style.color = 'var(--muted)';
|
||||||
var saveRow = document.getElementById('saa-dmi-save-row');
|
document.getElementById('saa-dmi-table').innerHTML = '';
|
||||||
status.textContent = 'Reading...';
|
fetch('/api/tools/saa-dmi', {cache:'no-store'})
|
||||||
status.style.color = 'var(--muted)';
|
.then(function(r){return r.json().then(function(d){if(!r.ok)throw new Error(d.error||('HTTP '+r.status));return d;});})
|
||||||
table.innerHTML = '';
|
.then(function(fields){
|
||||||
saveRow.style.display = 'none';
|
status.textContent = fields.length + ' field(s) loaded.';
|
||||||
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){
|
var rows = fields.map(function(f){
|
||||||
status.textContent = fields.length+' field(s) loaded.';
|
var val = dmiEsc(f.value||'');
|
||||||
var rows = fields.map(function(f){
|
return '<tr>'
|
||||||
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-size:13px;white-space:nowrap;padding-right:8px">'+saaDMIEsc(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="font-family:monospace;font-size:13px;white-space:nowrap;padding-right:8px">'+saaDMIEsc(f.shn)+'</td>'
|
+ '<td style="vertical-align:middle"><input class="dmi-inp" type="text" style="'+_dmiInputStyle+'"'
|
||||||
+'<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>'
|
+ ' data-shn="'+dmiEsc(f.shn)+'" data-original="'+val+'" value="'+val+'" oninput="dmiChanged(this)"></td>'
|
||||||
+'<td id="saa-dmi-dirty-'+saaDMIEsc(f.shn)+'" style="font-size:12px;color:var(--warn,#b45309);width:50px;padding-left:6px"></td>'
|
+ '<td class="dmi-act" style="display:none;white-space:nowrap;padding-left:6px;vertical-align:middle">'
|
||||||
+'</tr>';
|
+ '<button style="'+_dmiActBtnStyle+'color:var(--ok-fg,green);margin-right:3px" title="Save" onclick="dmiSave(this)">✓</button>'
|
||||||
}).join('');
|
+ '<button style="'+_dmiActBtnStyle+'color:var(--crit-fg,#9f3a38)" title="Cancel" onclick="dmiCancel(this)">✗</button>'
|
||||||
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>';
|
+ '<span class="dmi-msg" style="font-size:11px;margin-left:5px;color:var(--muted)"></span>'
|
||||||
}).catch(function(e){
|
+ '</td></tr>';
|
||||||
status.textContent = 'Error: '+e.message;
|
}).join('');
|
||||||
status.style.color = 'var(--crit-fg,#9f3a38)';
|
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) {
|
function dmiChanged(inp) {
|
||||||
var shn = inp.dataset.shn;
|
inp.closest('tr').querySelector('.dmi-act').style.display = inp.value !== inp.dataset.original ? '' : 'none';
|
||||||
var cell = document.getElementById('saa-dmi-dirty-'+shn);
|
|
||||||
if(cell)cell.textContent = inp.value!==inp.dataset.original?'changed':'';
|
|
||||||
saaDMIUpdateSaveBtn();
|
|
||||||
}
|
}
|
||||||
function saaDMIWaitTask(taskID) {
|
function dmiCancel(btn) {
|
||||||
var msg = document.getElementById('saa-dmi-save-msg');
|
var row = btn.closest('tr');
|
||||||
msg.textContent = 'Task '+taskID+' queued...';
|
var inp = row.querySelector('.dmi-inp');
|
||||||
msg.style.color = 'var(--muted)';
|
inp.value = inp.dataset.original;
|
||||||
var timer = setInterval(function(){
|
row.querySelector('.dmi-act').style.display = 'none';
|
||||||
fetch('/api/tasks').then(function(r){return r.json();}).then(function(tasks){
|
row.querySelector('.dmi-msg').textContent = '';
|
||||||
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 saaDMISave() {
|
function dmiSave(btn) {
|
||||||
var inputs = document.querySelectorAll('#saa-dmi-table input[data-original]');
|
var row = btn.closest('tr');
|
||||||
var changes = [];
|
var inp = row.querySelector('.dmi-inp');
|
||||||
inputs.forEach(function(inp){if(inp.value!==inp.dataset.original)changes.push({shn:inp.dataset.shn,value:inp.value});});
|
var msg = row.querySelector('.dmi-msg');
|
||||||
if(!changes.length)return;
|
var cancelBtn = row.querySelectorAll('.dmi-act button')[1];
|
||||||
var names = changes.map(function(c){return c.shn;}).join(', ');
|
if(!window.confirm('Apply DMI change for '+inp.dataset.shn+'?\nServer will need to reboot for changes to take effect.'))return;
|
||||||
if(!window.confirm('Apply DMI changes for: '+names+'?\n\nThe server will need to be rebooted for changes to take effect.'))return;
|
btn.disabled=true; cancelBtn.disabled=true;
|
||||||
var btn = document.getElementById('saa-dmi-save-btn');
|
msg.textContent='…'; msg.style.color='var(--muted)';
|
||||||
var msg = document.getElementById('saa-dmi-save-msg');
|
fetch('/api/tools/saa-dmi/write',{method:'POST',headers:{'Content-Type':'application/json'},
|
||||||
btn.disabled = true;
|
body:JSON.stringify({changes:[{shn:inp.dataset.shn,value:inp.value}]})})
|
||||||
msg.textContent = 'Submitting...';
|
.then(function(r){return r.json().then(function(d){if(!r.ok)throw new Error(d.error||('HTTP '+r.status));return d;});})
|
||||||
msg.style.color = 'var(--muted)';
|
.then(function(d){
|
||||||
fetch('/api/tools/saa-dmi/write',{
|
var poll=setInterval(function(){
|
||||||
method:'POST',
|
fetch('/api/tasks',{cache:'no-store'}).then(function(r){return r.json();}).then(function(tasks){
|
||||||
headers:{'Content-Type':'application/json'},
|
var t=(tasks||[]).find(function(x){return x.id===d.task_id;});
|
||||||
body:JSON.stringify({changes:changes})
|
if(!t)return;
|
||||||
}).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){
|
if(t.status==='done'){
|
||||||
saaDMIWaitTask(d.task_id);
|
clearInterval(poll);
|
||||||
}).catch(function(e){
|
inp.dataset.original=inp.value;
|
||||||
msg.textContent = 'Error: '+e.message;
|
row.querySelector('.dmi-act').style.display='none';
|
||||||
msg.style.color = 'var(--crit-fg,#9f3a38)';
|
msg.textContent='Saved. Reboot to apply.'; msg.style.color='var(--ok-fg,green)';
|
||||||
btn.disabled = false;
|
} 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>
|
</script>`
|
||||||
</div></div>`
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -332,6 +332,8 @@ func NewHandler(opts HandlerOptions) http.Handler {
|
|||||||
// System
|
// System
|
||||||
mux.HandleFunc("GET /api/system/ram-status", h.handleAPIRAMStatus)
|
mux.HandleFunc("GET /api/system/ram-status", h.handleAPIRAMStatus)
|
||||||
mux.HandleFunc("POST /api/system/install-to-ram", h.handleAPIInstallToRAM)
|
mux.HandleFunc("POST /api/system/install-to-ram", h.handleAPIInstallToRAM)
|
||||||
|
mux.HandleFunc("POST /api/system/reboot", h.handleAPISystemReboot)
|
||||||
|
mux.HandleFunc("POST /api/system/shutdown", h.handleAPISystemShutdown)
|
||||||
|
|
||||||
// Preflight
|
// Preflight
|
||||||
mux.HandleFunc("GET /api/preflight", h.handleAPIPreflight)
|
mux.HandleFunc("GET /api/preflight", h.handleAPIPreflight)
|
||||||
|
|||||||
Reference in New Issue
Block a user