주변 블루투스 스캔을 추가

This commit is contained in:
seo
2026-06-27 02:31:07 +09:00
parent caa583e25d
commit 6734801b52
5 changed files with 598 additions and 11 deletions
+106 -1
View File
@@ -16,6 +16,7 @@
wifi5: $('#wifi5'),
wifiTable: $('#wifiTable'),
wifiScan: $('#wifiScan'),
bluetoothScan: $('#bluetoothScan'),
statusHost: $('#statusHost'),
statusLoad: $('#statusLoad'),
statusUsers: $('#statusUsers'),
@@ -153,6 +154,18 @@
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',
trusted: 'Trusted',
blocked: 'Blocked',
observed: 'Observed',
ssid: 'SSID',
channel: 'Channel',
@@ -306,6 +319,18 @@
wifiScanFresh: '실시간',
wifiScanDisabledWithHistory: '스캔 꺼짐, 마지막 관측 목록 표시',
wifiScanObservedAll: '전체 BSSID 누적 관측',
bluetoothScan: '주변 Bluetooth 스캔',
bluetoothScanDisabled: '주변 Bluetooth 스캔이 꺼져 있습니다.',
bluetoothScanEmpty: '주변 Bluetooth 스캔 결과가 없습니다.',
bluetoothScanCached: '캐시',
bluetoothScanFresh: '실시간',
bluetoothScanDisabledWithHistory: '스캔 꺼짐, 마지막 관측 장치 표시',
bluetoothScanObservedAll: '전체 장치 누적 관측',
deviceName: '장치',
address: '주소',
paired: '페어링',
trusted: '신뢰',
blocked: '차단',
observed: '관측',
ssid: 'SSID',
channel: '채널',
@@ -1329,6 +1354,7 @@
}
renderWifiScan(data.wifi?.scan || null);
renderBluetoothScan(data.bluetooth?.scan || null);
}
function renderWifiScan(scan) {
@@ -1342,7 +1368,7 @@
const networks = Array.isArray(scan?.networks) ? scan.networks : [];
if (!scan || (scan.enabled === false && networks.length === 0)) {
els.wifiScan.innerHTML = `<div class="wifi-scan-empty">${escapeHtml(t('wifiScanDisabled'))}</div>`;
els.wifiScan.innerHTML = `<div class="scan-empty">${escapeHtml(t('wifiScanDisabled'))}</div>`;
return;
}
@@ -1404,6 +1430,85 @@
});
}
function renderBluetoothScan(scan) {
if (!els.bluetoothScan) 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><span class="wifi-signal ${bluetoothSignalClass(device.rssi)}">${escapeHtml(bluetoothRssiText(device.rssi))}</span></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 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 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}` : '방금';