주변 스캔 기능을 정리

This commit is contained in:
seo
2026-06-29 04:45:10 +09:00
parent 3be4d8838f
commit cd14bb2e5e
6 changed files with 13 additions and 1356 deletions
-244
View File
@@ -15,8 +15,6 @@
wifi24: $('#wifi24'),
wifi5: $('#wifi5'),
wifiTable: $('#wifiTable'),
wifiScan: $('#wifiScan'),
bluetoothScan: $('#bluetoothScan'),
signalTooltip: $('#signalTooltip'),
statusHost: $('#statusHost'),
statusLoad: $('#statusLoad'),
@@ -153,20 +151,6 @@
remaining: 'Remaining',
batteryVoltage: 'Battery Voltage',
wifiClients: 'WiFi Clients',
wifiScan: 'Nearby WiFi Scan',
wifiScanDisabled: 'Nearby WiFi scan is disabled.',
wifiScanEmpty: 'No nearby WiFi scan results.',
wifiScanCached: 'cached',
wifiScanFresh: 'fresh',
wifiScanDisabledWithHistory: 'scan disabled, showing last observed networks',
wifiScanObservedAll: 'all observed BSSID history',
bluetoothScan: 'Nearby Bluetooth Scan',
bluetoothScanDisabled: 'Nearby Bluetooth scan is disabled.',
bluetoothScanEmpty: 'No nearby Bluetooth scan results.',
bluetoothScanCached: 'cached',
bluetoothScanFresh: 'fresh',
bluetoothScanDisabledWithHistory: 'scan disabled, showing last observed devices',
bluetoothScanObservedAll: 'all observed devices',
deviceName: 'Device',
address: 'Address',
paired: 'Paired',
@@ -318,20 +302,6 @@
remaining: '잔여 시간',
batteryVoltage: '배터리 전압',
wifiClients: 'WiFi 클라이언트',
wifiScan: '주변 WiFi 스캔',
wifiScanDisabled: '주변 WiFi 스캔이 꺼져 있습니다.',
wifiScanEmpty: '주변 WiFi 스캔 결과가 없습니다.',
wifiScanCached: '캐시',
wifiScanFresh: '실시간',
wifiScanDisabledWithHistory: '스캔 꺼짐, 마지막 관측 목록 표시',
wifiScanObservedAll: '전체 BSSID 누적 관측',
bluetoothScan: '주변 Bluetooth 스캔',
bluetoothScanDisabled: '주변 Bluetooth 스캔이 꺼져 있습니다.',
bluetoothScanEmpty: '주변 Bluetooth 스캔 결과가 없습니다.',
bluetoothScanCached: '캐시',
bluetoothScanFresh: '실시간',
bluetoothScanDisabledWithHistory: '스캔 꺼짐, 마지막 관측 장치 표시',
bluetoothScanObservedAll: '전체 장치 누적 관측',
deviceName: '장치',
address: '주소',
paired: '페어링',
@@ -1197,14 +1167,6 @@
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);
@@ -1465,191 +1427,6 @@
if (tableWrap) {
tableWrap.scrollLeft = scrollLeft;
}
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 || '';
const wrap = section.querySelector('.table-wrap');
if (band && wrap) previousScroll[band] = wrap.scrollLeft;
});
const networks = Array.isArray(scan?.networks) ? scan.networks : [];
if (!scan || (scan.enabled === false && networks.length === 0)) {
els.wifiScan.innerHTML = `<div class="scan-empty">${escapeHtml(t('wifiScanDisabled'))}</div>`;
return;
}
const observedMode = scan.observed_mode === 'all' ? t('wifiScanObservedAll') : `${scan.observed_hours || '-'}h`;
const status = scan.enabled === false
? `${t('wifiScanDisabledWithHistory')} · ${observedMode}`
: `${scan.cached ? t('wifiScanCached') : t('wifiScanFresh')} · ${escapeHtml(scan.generated_at || '-')} · ${observedMode}`;
const bandOrder = ['2.4G', '5G', '6G', '기타'];
const extraBands = networks
.map(row => row.band || '기타')
.filter(band => !bandOrder.includes(band));
const bands = [...bandOrder, ...Array.from(new Set(extraBands))]
.filter((band, index) => index < 2 || networks.some(row => (row.band || '기타') === band));
const groups = bands.map(band => {
const bandRows = networks.filter(row => row.band === band);
return `
<section class="wifi-scan-band" data-band="${attr(band)}">
<h3>${escapeHtml(band)} <span>${bandRows.length}${state.lang === 'ko' ? '개' : ''}</span></h3>
<div class="table-wrap compact">
<table class="wifi-scan-table">
<thead><tr><th>${escapeHtml(t('ssid'))}</th><th>BSSID</th><th>${escapeHtml(t('channel'))}</th><th>${escapeHtml(t('signal'))}</th><th>${escapeHtml(t('security'))}</th><th>${escapeHtml(t('observed'))}</th><th>WiFi</th><th>폭</th><th>속도</th><th>기능</th></tr></thead>
<tbody>
${bandRows.length ? bandRows.map(row => {
const dbm = wifiSignalDbm(row.signal);
return `<tr>
<td>${escapeHtml(row.ssid || '-')}</td>
<td>${escapeHtml(row.bssid || '-')}</td>
<td>${escapeHtml(row.channel ?? '-')} ${row.freq ? `(${escapeHtml(row.freq)}MHz)` : ''}</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>
<td>${escapeHtml(row.channel_width || '-')}</td>
<td>${escapeHtml(row.rates || '-')}</td>
<td>${escapeHtml(row.capability || '-')}</td>
</tr>`;
}).join('') : `<tr><td colspan="10">${escapeHtml(t('wifiScanEmpty'))}</td></tr>`}
</tbody>
</table>
</div>
</section>
`;
}).join('');
els.wifiScan.innerHTML = `
<div class="wifi-scan-head">
<span>${status}</span>
</div>
<div class="wifi-scan-grid">${groups}</div>
`;
requestAnimationFrame(() => {
els.wifiScan.querySelectorAll('.wifi-scan-band').forEach(section => {
const band = section.dataset.band || '';
const wrap = section.querySelector('.table-wrap');
if (band && wrap && previousScroll[band] !== undefined) {
wrap.scrollLeft = previousScroll[band];
}
});
});
}
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 : [];
if (!scan || (scan.enabled === false && devices.length === 0)) {
els.bluetoothScan.innerHTML = `<div class="scan-empty">${escapeHtml(t('bluetoothScanDisabled'))}</div>`;
return;
}
const observedMode = scan.observed_mode === 'all' ? t('bluetoothScanObservedAll') : '-';
const status = scan.enabled === false
? `${t('bluetoothScanDisabledWithHistory')} · ${observedMode}`
: `${scan.cached ? t('bluetoothScanCached') : t('bluetoothScanFresh')} · ${escapeHtml(scan.generated_at || '-')} · ${observedMode}`;
const rows = devices.length ? devices.map(device => `
<tr>
<td>${escapeHtml(device.display_name || device.alias || device.name || '-')}</td>
<td>${escapeHtml(device.address || '-')}</td>
<td>${escapeHtml(device.address_type || '-')}</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>
<td>${escapeHtml(booleanStateText(device.trusted))}</td>
<td>${escapeHtml(booleanStateText(device.blocked))}</td>
<td>${escapeHtml(device.icon || '-')}</td>
<td>${escapeHtml(device.class || '-')}</td>
<td>${escapeHtml(joinDetailLines(device.uuids))}</td>
<td>${escapeHtml(joinDetailLines(device.manufacturer_data))}</td>
<td>${escapeHtml(joinDetailLines(device.service_data))}</td>
<td>${escapeHtml(device.modalias || '-')}</td>
<td>${escapeHtml(device.appearance || '-')}</td>
<td>${escapeHtml(wifiObservedText(device))}</td>
</tr>
`).join('') : `<tr><td colspan="17">${escapeHtml(t('bluetoothScanEmpty'))}</td></tr>`;
els.bluetoothScan.innerHTML = `
<div class="wifi-scan-head">
<span>${status}</span>
</div>
<div class="table-wrap compact">
<table class="wifi-scan-table">
<thead><tr><th>${escapeHtml(t('deviceName'))}</th><th>${escapeHtml(t('address'))}</th><th>주소 종류</th><th>RSSI</th><th>TxPower</th><th>${escapeHtml(t('connected'))}</th><th>${escapeHtml(t('paired'))}</th><th>${escapeHtml(t('trusted'))}</th><th>${escapeHtml(t('blocked'))}</th><th>Icon</th><th>Class</th><th>UUID</th><th>제조사 데이터</th><th>서비스 데이터</th><th>Modalias</th><th>Appearance</th><th>${escapeHtml(t('observed'))}</th></tr></thead>
<tbody>${rows}</tbody>
</table>
</div>
`;
requestAnimationFrame(() => {
const nextWrap = els.bluetoothScan.querySelector('.table-wrap');
if (nextWrap) nextWrap.scrollLeft = scrollLeft;
});
}
function bluetoothRssiText(value) {
return Number.isFinite(Number(value)) ? `${Number(value)} dBm` : '-';
}
function bluetoothSignalDbm(value) {
const numeric = Number(value);
return Number.isFinite(numeric) ? numeric : null;
}
function booleanStateText(value) {
if (value === true) return state.lang === 'ko' ? '예' : 'yes';
if (value === false) return state.lang === 'ko' ? '아니오' : 'no';
return '-';
}
function joinDetailLines(value) {
if (!Array.isArray(value) || value.length === 0) return '-';
return value.join(' / ');
}
function wifiObservedText(row) {
if (row?.observed_source === 'live') {
return row?.last_seen_at ? `${timeAgo(secondsSinceLocalTime(row.last_seen_at))} · ${row.last_seen_at}` : '방금';
}
if (row?.last_seen_at) {
return `${timeAgo(secondsSinceLocalTime(row.last_seen_at))} · ${row.last_seen_at}`;
}
if (row?.observed_age_seconds !== null && row?.observed_age_seconds !== undefined) {
return `${timeAgo(row.observed_age_seconds)} · -`;
}
return '-';
}
function secondsSinceLocalTime(value) {
const text = String(value || '').trim();
if (!text) return 0;
const parsed = new Date(text.includes('T') ? text : text.replace(' ', 'T'));
if (Number.isNaN(parsed.getTime())) return 0;
return Math.max(0, Math.floor((Date.now() - parsed.getTime()) / 1000));
}
function wifiStandardText(row) {
const parts = [];
if (row?.he) parts.push('WiFi 6');
if (row?.vht) parts.push('WiFi 5');
if (row?.ht) parts.push('WiFi 4');
return parts.join(' / ') || '-';
}
function renderSystemStatus(data) {
@@ -2503,27 +2280,6 @@
});
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);
});