Add guarded reboot control

This commit is contained in:
seo
2026-06-07 13:59:12 +09:00
parent d6b1322a8f
commit 4a02f7f354
6 changed files with 244 additions and 10 deletions
+150
View File
@@ -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' })) {