스캔 표 스크롤과 신호 표시를 다듬음
This commit is contained in:
+192
-27
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user