feat: redesign collection UI + add StopHostAfterCollect + TCP ping pre-probe
- Single "Подключиться" button flow: probe first, then show collect options - Power management checkboxes: power on before / stop after collect - Modal confirmation when enabling shutdown on already-powered-on host - StopHostAfterCollect flag: host shuts down only when explicitly requested - TCP ping (10 attempts, min 3 successes) before Redfish probe - Debug payloads checkbox (Oem/Ami/Inventory/Crc, off by default) - Remove platform_config BIOS settings collection (unreliable on AMI) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -91,14 +91,18 @@ function initApiSource() {
|
||||
}
|
||||
|
||||
const cancelJobButton = document.getElementById('cancel-job-btn');
|
||||
const checkButton = document.getElementById('api-check-btn');
|
||||
const collectOffButton = document.getElementById('api-collect-off-btn');
|
||||
const powerOnCollectButton = document.getElementById('api-power-on-collect-btn');
|
||||
const connectButton = document.getElementById('api-connect-btn');
|
||||
const collectButton = document.getElementById('api-collect-btn');
|
||||
const powerOffCheckbox = document.getElementById('api-power-off');
|
||||
const fieldNames = ['host', 'port', 'username', 'password'];
|
||||
|
||||
apiForm.addEventListener('submit', (event) => {
|
||||
event.preventDefault();
|
||||
startCollectionFromCurrentProbe(false);
|
||||
if (apiProbeResult && apiProbeResult.reachable) {
|
||||
startCollectionWithOptions();
|
||||
} else {
|
||||
startApiProbe();
|
||||
}
|
||||
});
|
||||
|
||||
if (cancelJobButton) {
|
||||
@@ -106,21 +110,29 @@ function initApiSource() {
|
||||
cancelCollectionJob();
|
||||
});
|
||||
}
|
||||
if (checkButton) {
|
||||
checkButton.addEventListener('click', () => {
|
||||
if (connectButton) {
|
||||
connectButton.addEventListener('click', () => {
|
||||
startApiProbe();
|
||||
});
|
||||
}
|
||||
if (collectOffButton) {
|
||||
collectOffButton.addEventListener('click', () => {
|
||||
clearApiPowerDecisionTimer();
|
||||
startCollectionFromCurrentProbe(false);
|
||||
if (collectButton) {
|
||||
collectButton.addEventListener('click', () => {
|
||||
startCollectionWithOptions();
|
||||
});
|
||||
}
|
||||
if (powerOnCollectButton) {
|
||||
powerOnCollectButton.addEventListener('click', () => {
|
||||
clearApiPowerDecisionTimer();
|
||||
startCollectionFromCurrentProbe(true);
|
||||
if (powerOffCheckbox) {
|
||||
powerOffCheckbox.addEventListener('change', () => {
|
||||
if (!powerOffCheckbox.checked) {
|
||||
return;
|
||||
}
|
||||
// If host was already on when probed, warn before enabling shutdown
|
||||
if (apiProbeResult && apiProbeResult.host_powered_on) {
|
||||
showConfirmModal(
|
||||
'Хост был включён до начала сбора. Вы уверены, что хотите выключить его после завершения сбора?',
|
||||
() => { /* confirmed — leave checked */ },
|
||||
() => { powerOffCheckbox.checked = false; }
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -151,11 +163,42 @@ function initApiSource() {
|
||||
renderCollectionJob();
|
||||
}
|
||||
|
||||
function showConfirmModal(message, onConfirm, onCancel) {
|
||||
const backdrop = document.createElement('div');
|
||||
backdrop.className = 'api-confirm-modal-backdrop';
|
||||
backdrop.innerHTML = `
|
||||
<div class="api-confirm-modal" role="dialog" aria-modal="true">
|
||||
<p>${escapeHtml(message)}</p>
|
||||
<div class="api-confirm-modal-actions">
|
||||
<button class="btn-cancel">Отмена</button>
|
||||
<button class="btn-confirm">Да, выключить</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(backdrop);
|
||||
|
||||
const close = () => document.body.removeChild(backdrop);
|
||||
backdrop.querySelector('.btn-cancel').addEventListener('click', () => {
|
||||
close();
|
||||
if (onCancel) onCancel();
|
||||
});
|
||||
backdrop.querySelector('.btn-confirm').addEventListener('click', () => {
|
||||
close();
|
||||
if (onConfirm) onConfirm();
|
||||
});
|
||||
backdrop.addEventListener('click', (e) => {
|
||||
if (e.target === backdrop) {
|
||||
close();
|
||||
if (onCancel) onCancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function startApiProbe() {
|
||||
const { isValid, payload, errors } = validateCollectForm();
|
||||
renderFormErrors(errors);
|
||||
if (!isValid) {
|
||||
renderApiConnectStatus(false, null);
|
||||
renderApiConnectStatus(false);
|
||||
resetApiProbeState();
|
||||
return;
|
||||
}
|
||||
@@ -163,7 +206,7 @@ function startApiProbe() {
|
||||
apiConnectPayload = payload;
|
||||
resetApiProbeState();
|
||||
setApiFormBlocked(true);
|
||||
renderApiConnectStatus(true, { ...payload, password: '***' });
|
||||
renderApiConnectStatus(true);
|
||||
|
||||
fetch('/api/collect/probe', {
|
||||
method: 'POST',
|
||||
@@ -181,7 +224,7 @@ function startApiProbe() {
|
||||
})
|
||||
.catch((err) => {
|
||||
resetApiProbeState();
|
||||
renderApiConnectStatus(false, null);
|
||||
renderApiConnectStatus(false);
|
||||
const status = document.getElementById('api-connect-status');
|
||||
if (status) {
|
||||
status.textContent = err.message || 'Проверка подключения не удалась';
|
||||
@@ -195,12 +238,11 @@ function startApiProbe() {
|
||||
});
|
||||
}
|
||||
|
||||
function startCollectionFromCurrentProbe(powerOnIfHostOff) {
|
||||
function startCollectionWithOptions() {
|
||||
const { isValid, payload, errors } = validateCollectForm();
|
||||
renderFormErrors(errors);
|
||||
if (!isValid) {
|
||||
renderApiConnectStatus(false, null);
|
||||
resetApiProbeState();
|
||||
renderApiConnectStatus(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -213,71 +255,78 @@ function startCollectionFromCurrentProbe(powerOnIfHostOff) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearApiPowerDecisionTimer();
|
||||
payload.power_on_if_host_off = Boolean(powerOnIfHostOff);
|
||||
const powerOnCheckbox = document.getElementById('api-power-on');
|
||||
const powerOffCheckbox = document.getElementById('api-power-off');
|
||||
const debugPayloads = document.getElementById('api-debug-payloads');
|
||||
payload.power_on_if_host_off = powerOnCheckbox ? powerOnCheckbox.checked : false;
|
||||
payload.stop_host_after_collect = powerOffCheckbox ? powerOffCheckbox.checked : false;
|
||||
payload.debug_payloads = debugPayloads ? debugPayloads.checked : false;
|
||||
startCollectionJob(payload);
|
||||
}
|
||||
|
||||
function renderApiProbeState() {
|
||||
const collectButton = document.getElementById('api-connect-btn');
|
||||
const connectButton = document.getElementById('api-connect-btn');
|
||||
const probeOptions = document.getElementById('api-probe-options');
|
||||
const status = document.getElementById('api-connect-status');
|
||||
const decision = document.getElementById('api-power-decision');
|
||||
const decisionText = document.getElementById('api-power-decision-text');
|
||||
if (!collectButton || !status || !decision || !decisionText) {
|
||||
const powerOnCheckbox = document.getElementById('api-power-on');
|
||||
const powerOffCheckbox = document.getElementById('api-power-off');
|
||||
if (!connectButton || !probeOptions || !status) {
|
||||
return;
|
||||
}
|
||||
|
||||
decision.classList.add('hidden');
|
||||
clearApiPowerDecisionTimer();
|
||||
collectButton.disabled = !apiProbeResult || !apiProbeResult.reachable;
|
||||
|
||||
if (!apiProbeResult || !apiProbeResult.reachable) {
|
||||
status.textContent = 'Проверка подключения не пройдена.';
|
||||
status.className = 'api-connect-status error';
|
||||
probeOptions.classList.add('hidden');
|
||||
connectButton.textContent = 'Подключиться';
|
||||
return;
|
||||
}
|
||||
|
||||
if (apiProbeResult.host_powered_on) {
|
||||
status.textContent = apiProbeResult.message || 'Связь с BMC есть, host включен.';
|
||||
const hostOn = apiProbeResult.host_powered_on;
|
||||
const powerControlAvailable = apiProbeResult.power_control_available;
|
||||
|
||||
if (hostOn) {
|
||||
status.textContent = apiProbeResult.message || 'Связь с BMC есть, host включён.';
|
||||
status.className = 'api-connect-status success';
|
||||
collectButton.disabled = false;
|
||||
return;
|
||||
} else {
|
||||
status.textContent = apiProbeResult.message || 'Связь с BMC есть, host выключен.';
|
||||
status.className = 'api-connect-status warning';
|
||||
}
|
||||
|
||||
status.textContent = apiProbeResult.message || 'Связь с BMC есть, host выключен.';
|
||||
status.className = 'api-connect-status warning';
|
||||
if (!apiProbeResult.power_control_available) {
|
||||
collectButton.disabled = false;
|
||||
return;
|
||||
}
|
||||
probeOptions.classList.remove('hidden');
|
||||
|
||||
decision.classList.remove('hidden');
|
||||
let secondsLeft = 5;
|
||||
const updateDecisionText = () => {
|
||||
decisionText.textContent = `Если не выбрать действие, сбор начнется без включения через ${secondsLeft} сек.`;
|
||||
};
|
||||
updateDecisionText();
|
||||
apiPowerDecisionTimer = window.setInterval(() => {
|
||||
secondsLeft -= 1;
|
||||
if (secondsLeft <= 0) {
|
||||
clearApiPowerDecisionTimer();
|
||||
startCollectionFromCurrentProbe(false);
|
||||
return;
|
||||
// "Включить" checkbox
|
||||
if (powerOnCheckbox) {
|
||||
if (hostOn) {
|
||||
// Host already on — checkbox is checked and disabled
|
||||
powerOnCheckbox.checked = true;
|
||||
powerOnCheckbox.disabled = true;
|
||||
} else {
|
||||
// Host off — default: checked (will power on), enabled
|
||||
powerOnCheckbox.checked = true;
|
||||
powerOnCheckbox.disabled = !powerControlAvailable;
|
||||
}
|
||||
updateDecisionText();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// "Выключить" checkbox — default: unchecked
|
||||
if (powerOffCheckbox) {
|
||||
powerOffCheckbox.checked = false;
|
||||
powerOffCheckbox.disabled = !powerControlAvailable;
|
||||
}
|
||||
|
||||
connectButton.textContent = 'Переподключиться';
|
||||
}
|
||||
|
||||
function resetApiProbeState() {
|
||||
apiProbeResult = null;
|
||||
clearApiPowerDecisionTimer();
|
||||
const collectButton = document.getElementById('api-connect-btn');
|
||||
const decision = document.getElementById('api-power-decision');
|
||||
if (collectButton) {
|
||||
collectButton.disabled = true;
|
||||
const connectButton = document.getElementById('api-connect-btn');
|
||||
const probeOptions = document.getElementById('api-probe-options');
|
||||
if (connectButton) {
|
||||
connectButton.textContent = 'Подключиться';
|
||||
}
|
||||
if (decision) {
|
||||
decision.classList.add('hidden');
|
||||
if (probeOptions) {
|
||||
probeOptions.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,7 +417,7 @@ function renderFormErrors(errors) {
|
||||
summary.innerHTML = `<strong>Исправьте ошибки в форме:</strong><ul>${messages.map(msg => `<li>${escapeHtml(msg)}</li>`).join('')}</ul>`;
|
||||
}
|
||||
|
||||
function renderApiConnectStatus(isValid, payload) {
|
||||
function renderApiConnectStatus(isValid) {
|
||||
const status = document.getElementById('api-connect-status');
|
||||
if (!status) {
|
||||
return;
|
||||
@@ -380,16 +429,8 @@ function renderApiConnectStatus(isValid, payload) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payloadPreview = { ...payload };
|
||||
if (payloadPreview.password) {
|
||||
payloadPreview.password = '***';
|
||||
}
|
||||
if (payloadPreview.token) {
|
||||
payloadPreview.token = '***';
|
||||
}
|
||||
|
||||
status.textContent = `Payload сформирован: ${JSON.stringify(payloadPreview)}`;
|
||||
status.className = 'api-connect-status success';
|
||||
status.textContent = 'Подключение...';
|
||||
status.className = 'api-connect-status info';
|
||||
}
|
||||
|
||||
function clearApiConnectStatus() {
|
||||
@@ -440,7 +481,7 @@ function startCollectionJob(payload) {
|
||||
.catch((err) => {
|
||||
setApiFormBlocked(false);
|
||||
clearApiConnectStatus();
|
||||
renderApiConnectStatus(false, null);
|
||||
renderApiConnectStatus(false);
|
||||
const status = document.getElementById('api-connect-status');
|
||||
if (status) {
|
||||
status.textContent = err.message || 'Ошибка запуска задачи';
|
||||
|
||||
Reference in New Issue
Block a user