Control 설정 모달 입력 보존 개선

This commit is contained in:
seo
2026-06-26 04:42:43 +09:00
parent da8c4e863d
commit ebef879634
5 changed files with 58 additions and 27 deletions
+6 -5
View File
@@ -3058,7 +3058,8 @@ function systemd_service_logs(string $unit, int $limit = 12): array
function custom_systemd_services(): array
{
$cachePath = '/tmp/control-custom-services.json';
if (is_file($cachePath) && filemtime($cachePath) >= time() - 5) {
$cacheSeconds = (int)setting_value('display.custom_service_cache_seconds');
if (is_file($cachePath) && filemtime($cachePath) >= time() - $cacheSeconds) {
$cached = json_decode((string)@file_get_contents($cachePath), true);
if (is_array($cached)) {
return $cached;
@@ -3104,7 +3105,7 @@ function custom_systemd_services(): array
foreach ($names as $name) {
$record = $records[$name] ?? [];
$logs = systemd_service_logs($name, 12);
$logs = systemd_service_logs($name, (int)setting_value('display.custom_service_log_lines'));
$rows[] = [
'name' => $name,
@@ -3259,7 +3260,7 @@ function collect_snapshot(bool $applyFan = true): array
], $battery['remaining']['seconds'] ?? null);
$history = array_slice($history, -(int)setting_value('battery.history_sync_limit'));
$processes = process_resource_data(6);
$processes = process_resource_data((int)setting_value('display.process_limit'));
$fanSpike = fan_spike_analysis($history, $fan, $system, $processes);
$shouldLogNotice = !empty($fanSpike['alert_started'])
@@ -3297,8 +3298,8 @@ function collect_snapshot(bool $applyFan = true): array
'processes' => $processes,
'custom_services' => custom_systemd_services(),
'fan_spike' => $fanSpike,
'fan_spike_history' => fan_spike_history(100),
'ha_notify_history' => ha_notify_log_rows(20),
'fan_spike_history' => fan_spike_history((int)setting_value('display.fan_notice_history_limit')),
'ha_notify_history' => ha_notify_log_rows((int)setting_value('display.ha_notify_history_limit')),
'settings' => settings_payload(),
];
+41 -20
View File
@@ -86,6 +86,7 @@
batteryTooltipText: '',
settingsPayload: null,
settingsOpen: false,
settingsDirty: false,
};
const storageKeys = {
@@ -521,9 +522,7 @@
cleanup(true);
};
const onCancel = () => cleanup(mode === 'confirm' ? false : null);
const onLayerClick = event => {
if (event.target === layer) onCancel();
};
const onLayerClick = () => {};
const onKeydown = event => {
if (event.key === 'Escape') {
event.preventDefault();
@@ -606,8 +605,13 @@
return json.data;
}
function renderSettings(payload = state.settingsPayload) {
function settingDomId(key) {
return `setting-${String(key).replace(/[^A-Za-z0-9_-]/g, '-')}`;
}
function renderSettings(payload = state.settingsPayload, options = {}) {
if (!els.settingsBody || !payload) return;
if (state.settingsOpen && state.settingsDirty && !options.force) return;
state.settingsPayload = payload;
const groups = payload.groups || {};
const byGroup = {};
@@ -618,21 +622,23 @@
});
const fieldHtml = item => {
const key = escapeHtml(item.key);
const id = escapeHtml(settingDomId(item.key));
const value = item.value ?? item.default ?? '';
const type = item.type || 'number';
if (type === 'boolean') {
const checked = value === true || value === 1 || value === '1' || value === 'true';
return `
<span class="settings-toggle">
<input data-setting-key="${key}" type="checkbox" value="1" ${checked ? 'checked' : ''}>
<span>${checked ? '켜기' : '끄기'}</span>
<input id="${id}" name="${key}" data-setting-key="${key}" type="checkbox" value="1" autocomplete="off" ${checked ? 'checked' : ''}>
<span class="settings-toggle-on">켜기</span>
<span class="settings-toggle-off">끄기</span>
</span>
`;
}
if (type === 'select') {
const options = item.options || {};
return `
<select data-setting-key="${key}" class="settings-select">
<select id="${id}" name="${key}" data-setting-key="${key}" class="settings-select" autocomplete="off">
${Object.entries(options).map(([optionValue, label]) => `
<option value="${escapeHtml(optionValue)}" ${String(optionValue) === String(value) ? 'selected' : ''}>${escapeHtml(label)}</option>
`).join('')}
@@ -642,19 +648,25 @@
if (type === 'text') {
return `
<input
id="${id}"
name="${key}"
data-setting-key="${key}"
type="text"
maxlength="${escapeHtml(item.max_length ?? 255)}"
autocomplete="off"
value="${escapeHtml(value)}">
`;
}
return `
<input
id="${id}"
name="${key}"
data-setting-key="${key}"
type="number"
min="${escapeHtml(item.min ?? '')}"
max="${escapeHtml(item.max ?? '')}"
step="${escapeHtml(item.step ?? '1')}"
autocomplete="off"
value="${escapeHtml(value)}">
`;
};
@@ -675,7 +687,7 @@
<h4>${escapeHtml(groups[group] || group)}</h4>
<div class="settings-grid">
${items.map(item => `
<label class="settings-field">
<div class="settings-field">
<span class="settings-label">
<span>${escapeHtml(item.label || item.key)}</span>
<span>${escapeHtml(item.unit || '')}</span>
@@ -684,17 +696,27 @@
${fieldHtml(item)}
<span class="settings-default">${t('settingsDefault')} ${escapeHtml(defaultValueText(item))}</span>
</span>
</label>
</div>
`).join('')}
</div>
</section>
`).join('');
els.settingsBody.querySelectorAll('[data-setting-key]').forEach(input => {
input.addEventListener('input', () => {
state.settingsDirty = true;
});
input.addEventListener('change', () => {
state.settingsDirty = true;
});
});
els.settingsBody.querySelectorAll('.settings-toggle input').forEach(input => {
input.addEventListener('change', () => {
const label = input.closest('.settings-toggle')?.querySelector('span');
if (label) label.textContent = input.checked ? '켜기' : '끄기';
const toggle = input.closest('.settings-toggle');
if (toggle) toggle.dataset.checked = input.checked ? '1' : '0';
});
input.closest('.settings-toggle')?.addEventListener('click', () => {
const toggle = input.closest('.settings-toggle');
toggle?.setAttribute('data-checked', input.checked ? '1' : '0');
toggle?.addEventListener('click', () => {
input.checked = !input.checked;
input.dispatchEvent(new Event('change'));
});
@@ -716,6 +738,7 @@
return;
}
state.settingsOpen = true;
state.settingsDirty = false;
els.settingsDialog.dataset.open = '1';
els.settingsDialog.setAttribute('aria-hidden', 'false');
}
@@ -723,6 +746,7 @@
function closeSettings() {
if (!els.settingsDialog) return;
state.settingsOpen = false;
state.settingsDirty = false;
delete els.settingsDialog.dataset.open;
els.settingsDialog.setAttribute('aria-hidden', 'true');
}
@@ -742,8 +766,9 @@
const data = await api('settings_save', {
settings: JSON.stringify(currentSettingsFormValues()),
});
state.settingsDirty = false;
render(data);
renderSettings(data.settings);
renderSettings(data.settings, { force: true });
notice(t('settingsSaved'), 'success');
} catch (e) {
console.error(e);
@@ -757,8 +782,9 @@
}
try {
const data = await api('settings_reset', {});
state.settingsDirty = false;
render(data);
renderSettings(data.settings);
renderSettings(data.settings, { force: true });
notice(t('settingsResetDone'), 'success');
} catch (e) {
console.error(e);
@@ -1809,7 +1835,7 @@
function render(data) {
if (data.settings) {
state.settingsPayload = data.settings;
if (state.settingsOpen) {
if (!state.settingsOpen) {
renderSettings(data.settings);
}
}
@@ -2062,11 +2088,6 @@
els.settingsClose?.addEventListener('click', closeSettings);
els.settingsSave?.addEventListener('click', saveSettings);
els.settingsReset?.addEventListener('click', resetSettings);
els.settingsDialog?.addEventListener('click', event => {
if (event.target === els.settingsDialog) {
closeSettings();
}
});
els.translateBtn?.addEventListener('click', () => {
applyLang(state.lang === 'ko' ? 'en' : 'ko');
refreshStatus();
+2 -2
View File
@@ -135,7 +135,7 @@ html[data-theme="light"] .details-panel{background:#edf3fa;border-color:#b8c7da}
.notice{position:fixed;right:20px;bottom:20px;min-width:220px;max-width:420px;padding:14px 16px;border-radius:14px;font-weight:500;background:var(--table-head);color:var(--text);border:1px solid var(--line);box-shadow:var(--shadow);z-index:99}.notice:empty{display:none}.notice[data-type=success]{border-color:rgba(53,196,107,.5)}.notice[data-type=error]{border-color:rgba(255,95,87,.5)}
.status-value.interactive{cursor:help;text-decoration:underline;text-decoration-style:dotted;text-decoration-thickness:1px;text-underline-offset:4px}.runtime-tooltip{position:fixed;left:0;top:0;z-index:110;display:none;width:min(520px,calc(100vw - 28px));max-height:calc(100vh - 28px);overflow:hidden;padding:14px 16px;border:1px solid rgba(84,101,128,.78);border-radius:16px;background:var(--card);color:var(--text);box-shadow:0 18px 48px rgba(0,0,0,.38);font-size:13px;line-height:1.55;white-space:pre-line;word-break:keep-all;overflow-wrap:anywhere;pointer-events:none}.runtime-tooltip[data-open="1"]{display:block}.runtime-tooltip::before{content:"";position:absolute;left:18px;top:-7px;width:12px;height:12px;border-left:1px solid rgba(84,101,128,.78);border-top:1px solid rgba(84,101,128,.78);background:var(--card);transform:rotate(45deg)}html[data-theme="light"] .runtime-tooltip{background:#f8fbff;border-color:#aebed2;color:#172033;box-shadow:0 18px 44px rgba(28,43,64,.18)}html[data-theme="light"] .runtime-tooltip::before{background:#f8fbff;border-color:#aebed2}
.dialog-layer{position:fixed;inset:0;display:none;align-items:center;justify-content:center;padding:22px;background:rgba(4,7,12,.68);backdrop-filter:blur(8px);z-index:120}.dialog-layer[data-open="1"]{display:flex}.dialog-box{width:min(430px,100%);border:1px solid rgba(84,101,128,.78);border-radius:20px;background:var(--card);box-shadow:0 24px 70px rgba(0,0,0,.42);padding:20px}.dialog-box h3{margin:0 0 8px;font-size:19px;font-weight:500}.dialog-message{margin:0 0 16px;color:var(--sub);line-height:1.5;white-space:pre-wrap}.dialog-input{width:100%;height:48px;margin-bottom:14px;border-radius:14px;border:1px solid var(--line);background:var(--input);color:var(--text);padding:0 14px;outline:none}.dialog-input:focus{outline:2px solid rgba(59,130,246,.65);outline-offset:2px}.dialog-actions{display:flex;justify-content:flex-end;gap:10px}.dialog-actions .btn{min-width:84px}.dialog-actions .btn.red{background:#c62828}
.settings-layer{align-items:stretch}.settings-box{width:min(980px,100%);max-height:calc(100vh - 44px);display:grid;grid-template-rows:auto minmax(0,1fr) auto;padding:0;overflow:hidden}.settings-head{display:flex;align-items:flex-start;justify-content:space-between;gap:14px;padding:18px 20px;border-bottom:1px solid var(--line)}.settings-sub{margin:0;color:var(--sub);font-size:13px}.settings-body{overflow:auto;padding:16px 20px;display:grid;gap:14px}.settings-group{border:1px solid var(--line);border-radius:16px;background:var(--card2);padding:14px}.settings-group h4{margin:0 0 12px;font-size:15px}.settings-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.settings-field{display:grid;gap:7px;min-width:0}.settings-label{display:flex;align-items:center;justify-content:space-between;gap:8px;color:var(--sub);font-size:13px}.settings-label span:last-child{white-space:nowrap;color:var(--sub)}.settings-control{display:flex;align-items:center;gap:9px}.settings-control input,.settings-select{width:100%;height:42px;border-radius:12px;border:1px solid var(--line);background:var(--input);color:var(--text);padding:0 12px;outline:none}.settings-control input:focus,.settings-select:focus{outline:2px solid rgba(59,130,246,.65);outline-offset:2px}.settings-toggle{display:flex;align-items:center;width:100%;height:42px;border:1px solid var(--line);border-radius:12px;background:var(--input);padding:4px;cursor:pointer}.settings-toggle input{position:absolute;opacity:0;pointer-events:none}.settings-toggle span{display:grid;place-items:center;width:72px;height:32px;border-radius:10px;background:#334155;color:#cbd5e1;font-weight:700;font-size:12px}.settings-toggle input:checked+span{background:var(--blue);color:#fff}.settings-default{font-size:12px;color:var(--sub);white-space:nowrap}.settings-actions{display:flex;justify-content:flex-end;gap:10px;padding:14px 20px;border-top:1px solid var(--line)}
.settings-layer{align-items:stretch}.settings-box{width:min(980px,100%);max-height:calc(100vh - 44px);display:grid;grid-template-rows:auto minmax(0,1fr) auto;padding:0;overflow:hidden}.settings-head{display:flex;align-items:flex-start;justify-content:space-between;gap:14px;padding:18px 20px;border-bottom:1px solid var(--line)}.settings-sub{margin:0;color:var(--sub);font-size:13px}.settings-body{overflow:auto;padding:16px 20px;display:grid;gap:14px}.settings-group{border:1px solid var(--line);border-radius:16px;background:var(--card2);padding:14px}.settings-group h4{margin:0 0 12px;font-size:15px}.settings-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.settings-field{display:grid;gap:7px;min-width:0}.settings-label{display:flex;align-items:center;justify-content:space-between;gap:8px;color:var(--sub);font-size:13px}.settings-label span:last-child{white-space:nowrap;color:var(--sub)}.settings-control{display:flex;align-items:center;gap:9px}.settings-control input,.settings-select{width:100%;height:42px;border-radius:12px;border:1px solid var(--line);background:var(--input);color:var(--text);padding:0 12px;outline:none}.settings-control input:focus,.settings-select:focus{outline:2px solid rgba(59,130,246,.65);outline-offset:2px}.settings-toggle{display:grid;grid-template-columns:1fr 1fr;align-items:center;width:100%;height:42px;border:1px solid var(--line);border-radius:12px;background:var(--input);padding:4px;cursor:pointer;gap:4px}.settings-toggle input{position:absolute;opacity:0;pointer-events:none}.settings-toggle span{display:grid;place-items:center;min-width:0;height:32px;border-radius:10px;color:var(--sub);font-weight:700;font-size:12px;transition:background .12s ease,color .12s ease}.settings-toggle[data-checked="1"] .settings-toggle-on,.settings-toggle[data-checked="0"] .settings-toggle-off{background:var(--blue);color:#fff}.settings-toggle[data-checked="0"] .settings-toggle-off{background:#64748b}.settings-default{font-size:12px;color:var(--sub);white-space:nowrap}.settings-actions{display:flex;justify-content:flex-end;gap:10px;padding:14px 20px;border-top:1px solid var(--line)}
@media(max-width:1320px){.chart-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(max-width:1100px){.layout{grid-template-columns:1fr}.chart-grid,.resource-grid{grid-template-columns:1fr}.topbar{align-items:flex-start;flex-direction:column}.topbar-right{width:100%}.topbar-right .btn{flex:1}.stat-grid{grid-template-columns:1fr}.resource-head{align-items:flex-start;flex-direction:column}.baseline-pill{text-align:left;white-space:normal}}
@media(max-width:720px){.settings-grid{grid-template-columns:1fr}.settings-head{align-items:stretch;flex-direction:column}.settings-actions{display:grid;grid-template-columns:1fr 1fr}.settings-box{max-height:calc(100vh - 24px)}}
@@ -364,7 +364,7 @@ html[data-theme="light"] .details-panel{background:#edf3fa;border-color:#b8c7da}
<div class="dialog-box" role="dialog" aria-modal="true" aria-labelledby="customDialogTitle">
<h3 id="customDialogTitle">확인</h3>
<p id="customDialogMessage" class="dialog-message"></p>
<input id="customDialogInput" class="dialog-input" type="text" autocomplete="off" hidden>
<input id="customDialogInput" name="custom_dialog_input" class="dialog-input" type="text" autocomplete="off" hidden>
<div class="dialog-actions">
<button id="customDialogCancel" class="btn secondary" type="button">취소</button>
<button id="customDialogOk" class="btn" type="button">확인</button>