스캔 표 스크롤과 신호 표시를 다듬음

This commit is contained in:
seo
2026-06-27 02:50:11 +09:00
parent 6734801b52
commit 9936ae6985
3 changed files with 199 additions and 33 deletions
+192 -27
View File
@@ -17,6 +17,7 @@
wifiTable: $('#wifiTable'),
wifiScan: $('#wifiScan'),
bluetoothScan: $('#bluetoothScan'),
signalTooltip: $('#signalTooltip'),
statusHost: $('#statusHost'),
statusLoad: $('#statusLoad'),
statusUsers: $('#statusUsers'),
@@ -86,6 +87,11 @@
theme: 'dark',
batteryTooltipOpen: false,
batteryTooltipText: '',
signalTooltipOpen: false,
signalTooltipTarget: null,
signalTooltipKey: '',
signalTooltipText: '',
scanScrollHoldUntil: 0,
settingsPayload: null,
settingsOpen: false,
settingsDirty: false,
@@ -1108,6 +1114,104 @@
els.batteryRuntimeTooltip.setAttribute('aria-hidden', 'true');
}
function signalTooltipTarget() {
if (state.signalTooltipKey && typeof document.querySelector === 'function') {
const selector = `[data-signal-key="${cssEscape(state.signalTooltipKey)}"]`;
const fresh = document.querySelector(selector);
if (fresh) {
state.signalTooltipTarget = fresh;
return fresh;
}
}
return state.signalTooltipTarget?.isConnected ? state.signalTooltipTarget : null;
}
function signalTooltipText(target = signalTooltipTarget()) {
if (!target) return '-';
const label = target.dataset.signalLabel || target.textContent || '-';
const dbm = target.dataset.signalDbm || '-';
return `${label}\n${dbm}`;
}
function positionSignalTooltip() {
if (!state.signalTooltipOpen || !els.signalTooltip) {
return;
}
const target = signalTooltipTarget();
if (!target) {
hideSignalTooltip();
return;
}
const targetRect = target.getBoundingClientRect();
const tip = els.signalTooltip;
const margin = 14;
tip.style.left = '0px';
tip.style.top = '0px';
const tipRect = tip.getBoundingClientRect();
let left = targetRect.left + (targetRect.width / 2) - (tipRect.width / 2);
let top = targetRect.bottom + 10;
if (left + tipRect.width > window.innerWidth - margin) {
left = window.innerWidth - tipRect.width - margin;
}
if (left < margin) {
left = margin;
}
if (top + tipRect.height > window.innerHeight - margin) {
top = targetRect.top - tipRect.height - 10;
}
if (top < margin) {
top = margin;
}
tip.style.left = `${Math.round(left)}px`;
tip.style.top = `${Math.round(top)}px`;
}
function updateSignalTooltip() {
if (!els.signalTooltip) return;
state.signalTooltipText = signalTooltipText();
els.signalTooltip.textContent = state.signalTooltipText || '-';
if (state.signalTooltipOpen) {
requestAnimationFrame(positionSignalTooltip);
}
}
function showSignalTooltip(target) {
if (!els.signalTooltip || !target || target.classList.contains('unknown')) return;
state.signalTooltipOpen = true;
state.signalTooltipTarget = target;
state.signalTooltipKey = target.dataset.signalKey || '';
els.signalTooltip.dataset.open = '1';
els.signalTooltip.setAttribute('aria-hidden', 'false');
updateSignalTooltip();
}
function hideSignalTooltip() {
if (!els.signalTooltip) return;
state.signalTooltipOpen = false;
state.signalTooltipTarget = null;
state.signalTooltipKey = '';
delete els.signalTooltip.dataset.open;
els.signalTooltip.setAttribute('aria-hidden', 'true');
}
function holdScanRender(ms = 900) {
state.scanScrollHoldUntil = Math.max(state.scanScrollHoldUntil, Date.now() + ms);
}
function scanRenderHeld() {
return Date.now() < state.scanScrollHoldUntil;
}
function cssEscape(value) {
if (window.CSS?.escape) {
return window.CSS.escape(value);
}
return String(value).replace(/["\\]/g, '\\$&');
}
function renderTop(data) {
setText(els.updated, data.generated_at || '-');
setText(els.temp, Number(data.system?.temp_c || 0).toFixed(1) + '°C');
@@ -1159,9 +1263,7 @@
}
const dbm = wifiSignalDbm(row[key]);
const cls = wifiSignalClass(dbm);
const title = dbm === null ? '' : `title="${attr(signalQualityText(dbm))}"`;
return `<td><span class="wifi-signal ${cls}" ${title}>${escapeHtml(row[key] || '-')}</span></td>`;
return `<td>${signalBadgeHtml(dbm, row[key] || '', `client:${row.mac || row.ip || row.hostname || ''}`)}</td>`;
}
function wifiSignalDbm(value) {
@@ -1169,21 +1271,32 @@
return match ? Number(match[0]) : null;
}
function wifiSignalClass(dbm) {
function signalQuality(dbm) {
if (dbm === null || !Number.isFinite(dbm)) return 'unknown';
if (dbm >= -55) return 'excellent';
if (dbm >= -67) return 'good';
if (dbm >= -76) return 'fair';
if (dbm >= -84) return 'weak';
return 'bad';
if (dbm >= -45) return { className: 'best', label: '최상' };
if (dbm >= -55) return { className: 'excellent', label: '매우 좋음' };
if (dbm >= -65) return { className: 'good', label: '좋음' };
if (dbm >= -72) return { className: 'fine', label: '양호' };
if (dbm >= -80) return { className: 'normal', label: '보통' };
if (dbm >= -88) return { className: 'weak', label: '약함' };
return { className: 'bad', label: '매우 약함' };
}
function signalQualityText(dbm) {
if (dbm >= -55) return '매우 강함';
if (dbm >= -67) return '강함';
if (dbm >= -76) return '보통';
if (dbm >= -84) return '약함';
return '매우 약함';
function signalDbmText(dbm, raw = '') {
if (dbm !== null && Number.isFinite(dbm)) {
return `${Number(dbm).toFixed(Number.isInteger(dbm) ? 0 : 1)} dBm`;
}
return String(raw || '').trim() || '-';
}
function signalBadgeHtml(dbm, raw = '', key = '') {
const quality = signalQuality(dbm);
if (typeof quality === 'string') {
return `<span class="wifi-signal unknown">-</span>`;
}
const keyAttr = key ? ` data-signal-key="${attr(key)}"` : '';
return `<span class="wifi-signal ${quality.className}" tabindex="0" role="button" aria-describedby="signalTooltip" data-signal-dbm="${attr(signalDbmText(dbm, raw))}" data-signal-label="${attr(quality.label)}"${keyAttr}>${escapeHtml(quality.label)}</span>`;
}
async function editWifiHostname(mac, currentName) {
@@ -1353,12 +1466,17 @@
tableWrap.scrollLeft = scrollLeft;
}
renderWifiScan(data.wifi?.scan || null);
renderBluetoothScan(data.bluetooth?.scan || null);
if (data.wifi && Object.prototype.hasOwnProperty.call(data.wifi, 'scan')) {
renderWifiScan(data.wifi.scan || null);
}
if (data.bluetooth && Object.prototype.hasOwnProperty.call(data.bluetooth, 'scan')) {
renderBluetoothScan(data.bluetooth.scan || null);
}
}
function renderWifiScan(scan) {
if (!els.wifiScan) return;
if (scanRenderHeld()) return;
const previousScroll = {};
els.wifiScan.querySelectorAll('.wifi-scan-band').forEach(section => {
const band = section.dataset.band || '';
@@ -1397,7 +1515,7 @@
<td>${escapeHtml(row.ssid || '-')}</td>
<td>${escapeHtml(row.bssid || '-')}</td>
<td>${escapeHtml(row.channel ?? '-')} ${row.freq ? `(${escapeHtml(row.freq)}MHz)` : ''}</td>
<td><span class="wifi-signal ${wifiSignalClass(dbm)}">${escapeHtml(row.signal || '-')}</span></td>
<td>${signalBadgeHtml(dbm, row.signal || '', `scan:${row.bssid || row.ssid || ''}`)}</td>
<td>${escapeHtml(row.security || '-')}</td>
<td>${escapeHtml(wifiObservedText(row))}</td>
<td>${escapeHtml(wifiStandardText(row))}</td>
@@ -1432,6 +1550,7 @@
function renderBluetoothScan(scan) {
if (!els.bluetoothScan) return;
if (scanRenderHeld()) return;
const wrap = els.bluetoothScan.querySelector('.table-wrap');
const scrollLeft = wrap ? wrap.scrollLeft : 0;
const devices = Array.isArray(scan?.devices) ? scan.devices : [];
@@ -1450,7 +1569,7 @@
<td>${escapeHtml(device.display_name || device.alias || device.name || '-')}</td>
<td>${escapeHtml(device.address || '-')}</td>
<td>${escapeHtml(device.address_type || '-')}</td>
<td><span class="wifi-signal ${bluetoothSignalClass(device.rssi)}">${escapeHtml(bluetoothRssiText(device.rssi))}</span></td>
<td>${signalBadgeHtml(bluetoothSignalDbm(device.rssi), bluetoothRssiText(device.rssi), `bt:${device.address || device.name || ''}`)}</td>
<td>${escapeHtml(device.tx_power ?? '-')}</td>
<td>${escapeHtml(booleanStateText(device.connected))}</td>
<td>${escapeHtml(booleanStateText(device.paired))}</td>
@@ -1488,14 +1607,9 @@
return Number.isFinite(Number(value)) ? `${Number(value)} dBm` : '-';
}
function bluetoothSignalClass(value) {
const dbm = Number(value);
if (!Number.isFinite(dbm)) return 'unknown';
if (dbm >= -55) return 'excellent';
if (dbm >= -67) return 'good';
if (dbm >= -75) return 'fair';
if (dbm >= -85) return 'weak';
return 'bad';
function bluetoothSignalDbm(value) {
const numeric = Number(value);
return Number.isFinite(numeric) ? numeric : null;
}
function booleanStateText(value) {
@@ -2124,6 +2238,7 @@
renderSystemStatus(data);
renderWifi(data);
renderCharts(data);
updateSignalTooltip();
state.fanCauseTick = (state.fanCauseTick + 1) % 2;
@@ -2359,6 +2474,56 @@
els.statusBatteryRemaining?.addEventListener('blur', hideBatteryTooltip);
window.addEventListener('resize', positionBatteryTooltip);
window.addEventListener('scroll', positionBatteryTooltip, true);
document.addEventListener('mouseover', event => {
const target = event.target?.closest?.('.wifi-signal[data-signal-dbm]');
if (target) showSignalTooltip(target);
});
document.addEventListener('mouseout', event => {
const target = event.target?.closest?.('.wifi-signal[data-signal-dbm]');
if (!target) return;
const related = event.relatedTarget;
if (related && target.contains(related)) return;
hideSignalTooltip();
});
document.addEventListener('focusin', event => {
const target = event.target?.closest?.('.wifi-signal[data-signal-dbm]');
if (target) showSignalTooltip(target);
});
document.addEventListener('focusout', event => {
const target = event.target?.closest?.('.wifi-signal[data-signal-dbm]');
if (target) hideSignalTooltip();
});
document.addEventListener('pointerdown', event => {
const target = event.target?.closest?.('.wifi-signal[data-signal-dbm]');
if (target) {
showSignalTooltip(target);
return;
}
hideSignalTooltip();
});
window.addEventListener('resize', positionSignalTooltip);
window.addEventListener('scroll', positionSignalTooltip, true);
document.addEventListener('pointerdown', event => {
if (event.target?.closest?.('.nearby-scan .table-wrap')) {
holdScanRender(1200);
}
}, true);
document.addEventListener('pointermove', event => {
if (event.target?.closest?.('.nearby-scan .table-wrap')) {
holdScanRender(900);
}
}, true);
document.addEventListener('pointerup', () => {
holdScanRender(450);
}, true);
document.addEventListener('pointercancel', () => {
holdScanRender(450);
}, true);
document.addEventListener('scroll', event => {
if (event.target?.closest?.('.nearby-scan .table-wrap')) {
holdScanRender(900);
}
}, true);
[els.secondaryChartDetails, els.processDetails, els.diagnosticDetails].forEach(node => {
node?.addEventListener('toggle', resizeChartsSoon);
});