Add guarded reboot control
This commit is contained in:
@@ -29,6 +29,13 @@
|
||||
pushStatus: $('#pushStatus'),
|
||||
pushDeviceList: $('#pushDeviceList'),
|
||||
pushHealthcheckBtn: $('#pushHealthcheckBtn'),
|
||||
rebootBtn: $('#rebootBtn'),
|
||||
customDialog: $('#customDialog'),
|
||||
customDialogTitle: $('#customDialogTitle'),
|
||||
customDialogMessage: $('#customDialogMessage'),
|
||||
customDialogInput: $('#customDialogInput'),
|
||||
customDialogCancel: $('#customDialogCancel'),
|
||||
customDialogOk: $('#customDialogOk'),
|
||||
processCpuTable: $('#processCpuTable'),
|
||||
processMemoryTable: $('#processMemoryTable'),
|
||||
dmesgToggle: $('#dmesgToggle'),
|
||||
@@ -111,6 +118,97 @@
|
||||
}, 2600);
|
||||
}
|
||||
|
||||
function customDialog(options = {}) {
|
||||
const layer = els.customDialog;
|
||||
const input = els.customDialogInput;
|
||||
const ok = els.customDialogOk;
|
||||
const cancel = els.customDialogCancel;
|
||||
if (!layer || !input || !ok || !cancel) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
const mode = options.mode || 'alert';
|
||||
const needsInput = mode === 'prompt';
|
||||
const previousFocus = document.activeElement;
|
||||
let settled = false;
|
||||
|
||||
const cleanup = value => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
layer.dataset.open = '0';
|
||||
layer.setAttribute('aria-hidden', 'true');
|
||||
ok.classList.remove('red');
|
||||
ok.textContent = '확인';
|
||||
cancel.textContent = '취소';
|
||||
input.value = '';
|
||||
input.hidden = true;
|
||||
input.type = 'text';
|
||||
ok.removeEventListener('click', onOk);
|
||||
cancel.removeEventListener('click', onCancel);
|
||||
layer.removeEventListener('click', onLayerClick);
|
||||
document.removeEventListener('keydown', onKeydown);
|
||||
if (previousFocus && typeof previousFocus.focus === 'function') {
|
||||
previousFocus.focus();
|
||||
}
|
||||
resolve(value);
|
||||
};
|
||||
|
||||
const onOk = () => {
|
||||
if (needsInput) {
|
||||
cleanup(input.value);
|
||||
return;
|
||||
}
|
||||
cleanup(true);
|
||||
};
|
||||
const onCancel = () => cleanup(mode === 'confirm' ? false : null);
|
||||
const onLayerClick = event => {
|
||||
if (event.target === layer) onCancel();
|
||||
};
|
||||
const onKeydown = event => {
|
||||
if (event.key === 'Escape') onCancel();
|
||||
if (event.key === 'Enter' && (document.activeElement === input || !needsInput)) onOk();
|
||||
};
|
||||
|
||||
els.customDialogTitle.textContent = options.title || '확인';
|
||||
els.customDialogMessage.textContent = options.message || '';
|
||||
ok.textContent = options.okText || '확인';
|
||||
cancel.textContent = options.cancelText || '취소';
|
||||
ok.classList.toggle('red', options.danger === true);
|
||||
cancel.hidden = mode === 'alert';
|
||||
input.hidden = !needsInput;
|
||||
input.type = options.inputType || 'text';
|
||||
input.placeholder = options.placeholder || '';
|
||||
input.autocomplete = options.autocomplete || 'off';
|
||||
input.value = options.defaultValue || '';
|
||||
|
||||
ok.addEventListener('click', onOk);
|
||||
cancel.addEventListener('click', onCancel);
|
||||
layer.addEventListener('click', onLayerClick);
|
||||
document.addEventListener('keydown', onKeydown);
|
||||
|
||||
layer.dataset.open = '1';
|
||||
layer.setAttribute('aria-hidden', 'false');
|
||||
setTimeout(() => (needsInput ? input : ok).focus(), 20);
|
||||
});
|
||||
}
|
||||
|
||||
window.customAlert = (message, options = {}) => customDialog(Object.assign({
|
||||
mode: 'alert',
|
||||
title: '알림',
|
||||
message,
|
||||
}, options));
|
||||
window.customConfirm = (message, options = {}) => customDialog(Object.assign({
|
||||
mode: 'confirm',
|
||||
title: '확인',
|
||||
message,
|
||||
}, options));
|
||||
window.customPrompt = (message, options = {}) => customDialog(Object.assign({
|
||||
mode: 'prompt',
|
||||
title: '입력',
|
||||
message,
|
||||
}, options));
|
||||
|
||||
async function api(action, body = null) {
|
||||
const opts = {
|
||||
method: body ? 'POST' : 'GET',
|
||||
@@ -1081,6 +1179,55 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function requestReboot() {
|
||||
const phrase = await window.customPrompt('재부팅을 계속하려면 아래에 재부팅을 입력하세요.', {
|
||||
title: '시스템 재부팅',
|
||||
okText: '다음',
|
||||
danger: true,
|
||||
placeholder: '재부팅',
|
||||
autocomplete: 'off',
|
||||
});
|
||||
|
||||
if (phrase === null) return;
|
||||
if (String(phrase).trim() !== '재부팅') {
|
||||
await window.customAlert('확인 단어가 일치하지 않습니다.', {
|
||||
title: '재부팅 취소',
|
||||
danger: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const password = await window.customPrompt('관리자 암호를 다시 입력하세요.', {
|
||||
title: '관리자 확인',
|
||||
okText: '재부팅',
|
||||
danger: true,
|
||||
inputType: 'password',
|
||||
autocomplete: 'current-password',
|
||||
});
|
||||
|
||||
if (password === null) return;
|
||||
|
||||
try {
|
||||
els.rebootBtn.disabled = true;
|
||||
notice('Reboot command sending...', 'info');
|
||||
await api('reboot', {
|
||||
phrase: '재부팅',
|
||||
password,
|
||||
});
|
||||
await window.customAlert('재부팅 명령을 전송했습니다. 잠시 후 연결이 끊어질 수 있습니다.', {
|
||||
title: '재부팅 시작',
|
||||
okText: '확인',
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
els.rebootBtn.disabled = false;
|
||||
await window.customAlert(e.message || 'reboot failed', {
|
||||
title: '재부팅 실패',
|
||||
danger: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (els.fanSlider && els.fanSliderValue) {
|
||||
els.fanSlider.addEventListener('input', () => {
|
||||
markFanDirty();
|
||||
@@ -1104,6 +1251,9 @@
|
||||
els.dmesgToggle?.addEventListener('click', () => {
|
||||
setDmesgOpen(!state.dmesgOpen);
|
||||
});
|
||||
els.rebootBtn?.addEventListener('click', () => {
|
||||
requestReboot();
|
||||
});
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (!document.hidden) {
|
||||
if (!sendWs({ type: 'status_refresh' })) {
|
||||
|
||||
Reference in New Issue
Block a user