Control WiFi 관측값 유지와 스크롤 보존
This commit is contained in:
+173
-13
@@ -2860,6 +2860,171 @@ function wifi_scan_failure_reason(int $code, string $out): string
|
||||
return '스캔 결과가 비어 있습니다. AP 모드, 드라이버 제한, 권한 문제 중 하나일 수 있습니다.';
|
||||
}
|
||||
|
||||
function save_wifi_scan_observations(array $networks): void
|
||||
{
|
||||
if ($networks === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = db()->prepare("
|
||||
INSERT INTO wifi_scan_observations
|
||||
(bssid, iface, band, ssid, freq, channel, signal_dbm, signal_text, security_text, capability_text, rates_text, channel_width, ht, vht, he, first_seen_at, last_seen_at)
|
||||
VALUES
|
||||
(:bssid, :iface, :band, :ssid, :freq, :channel, :signal_dbm, :signal_text, :security_text, :capability_text, :rates_text, :channel_width, :ht, :vht, :he, NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
iface = VALUES(iface),
|
||||
band = VALUES(band),
|
||||
ssid = VALUES(ssid),
|
||||
freq = VALUES(freq),
|
||||
channel = VALUES(channel),
|
||||
signal_dbm = VALUES(signal_dbm),
|
||||
signal_text = VALUES(signal_text),
|
||||
security_text = VALUES(security_text),
|
||||
capability_text = VALUES(capability_text),
|
||||
rates_text = VALUES(rates_text),
|
||||
channel_width = VALUES(channel_width),
|
||||
ht = VALUES(ht),
|
||||
vht = VALUES(vht),
|
||||
he = VALUES(he),
|
||||
last_seen_at = NOW()
|
||||
");
|
||||
|
||||
foreach ($networks as $network) {
|
||||
$bssid = strtolower((string)($network['bssid'] ?? ''));
|
||||
if (!wifi_valid_mac($bssid)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$stmt->execute([
|
||||
':bssid' => $bssid,
|
||||
':iface' => mb_substr((string)($network['iface'] ?? ''), 0, 32),
|
||||
':band' => mb_substr((string)($network['band'] ?? ''), 0, 16),
|
||||
':ssid' => mb_substr((string)($network['ssid'] ?? ''), 0, 255),
|
||||
':freq' => is_numeric($network['freq'] ?? null) ? (int)$network['freq'] : null,
|
||||
':channel' => is_numeric($network['channel'] ?? null) ? (int)$network['channel'] : null,
|
||||
':signal_dbm' => is_numeric($network['signal_dbm'] ?? null) ? (float)$network['signal_dbm'] : null,
|
||||
':signal_text' => mb_substr((string)($network['signal'] ?? ''), 0, 32),
|
||||
':security_text' => mb_substr((string)($network['security'] ?? ''), 0, 255),
|
||||
':capability_text' => mb_substr((string)($network['capability'] ?? ''), 0, 255),
|
||||
':rates_text' => mb_substr((string)($network['rates'] ?? ''), 0, 255),
|
||||
':channel_width' => mb_substr((string)($network['channel_width'] ?? ''), 0, 64),
|
||||
':ht' => !empty($network['ht']) ? 1 : 0,
|
||||
':vht' => !empty($network['vht']) ? 1 : 0,
|
||||
':he' => !empty($network['he']) ? 1 : 0,
|
||||
]);
|
||||
}
|
||||
} catch (Throwable) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function wifi_scan_observed_networks(int $hours, int $limit): array
|
||||
{
|
||||
try {
|
||||
$hours = max(1, min(720, $hours));
|
||||
$limit = max(1, min(500, $limit));
|
||||
$stmt = db()->query("
|
||||
SELECT
|
||||
bssid,
|
||||
iface,
|
||||
band,
|
||||
ssid,
|
||||
freq,
|
||||
channel,
|
||||
signal_dbm,
|
||||
signal_text,
|
||||
security_text,
|
||||
capability_text,
|
||||
rates_text,
|
||||
channel_width,
|
||||
ht,
|
||||
vht,
|
||||
he,
|
||||
first_seen_at,
|
||||
last_seen_at,
|
||||
TIMESTAMPDIFF(SECOND, last_seen_at, NOW()) AS observed_age_seconds
|
||||
FROM wifi_scan_observations
|
||||
WHERE last_seen_at >= DATE_SUB(NOW(), INTERVAL {$hours} HOUR)
|
||||
ORDER BY
|
||||
FIELD(band, '2.4G', '5G', '6G', '기타'),
|
||||
signal_dbm DESC,
|
||||
last_seen_at DESC
|
||||
LIMIT {$limit}
|
||||
");
|
||||
} catch (Throwable) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
foreach ($stmt->fetchAll() as $row) {
|
||||
$signalDbm = is_numeric($row['signal_dbm'] ?? null) ? (float)$row['signal_dbm'] : null;
|
||||
$rows[] = [
|
||||
'bssid' => (string)($row['bssid'] ?? ''),
|
||||
'iface' => (string)($row['iface'] ?? ''),
|
||||
'band' => (string)($row['band'] ?? ''),
|
||||
'ssid' => (string)($row['ssid'] ?? ''),
|
||||
'freq' => is_numeric($row['freq'] ?? null) ? (int)$row['freq'] : null,
|
||||
'channel' => is_numeric($row['channel'] ?? null) ? (int)$row['channel'] : null,
|
||||
'signal_dbm' => $signalDbm,
|
||||
'signal' => (string)($row['signal_text'] ?? ($signalDbm === null ? 'N/A' : number_format($signalDbm, 1) . ' dBm')),
|
||||
'security' => (string)($row['security_text'] ?? ''),
|
||||
'capability' => (string)($row['capability_text'] ?? ''),
|
||||
'rates' => (string)($row['rates_text'] ?? ''),
|
||||
'channel_width' => (string)($row['channel_width'] ?? ''),
|
||||
'ht' => !empty($row['ht']),
|
||||
'vht' => !empty($row['vht']),
|
||||
'he' => !empty($row['he']),
|
||||
'first_seen_at' => (string)($row['first_seen_at'] ?? ''),
|
||||
'last_seen_at' => (string)($row['last_seen_at'] ?? ''),
|
||||
'observed_age_seconds' => is_numeric($row['observed_age_seconds'] ?? null) ? (int)$row['observed_age_seconds'] : null,
|
||||
'observed_source' => 'db',
|
||||
];
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
function merge_wifi_scan_networks(array $freshNetworks, array $observedNetworks): array
|
||||
{
|
||||
$merged = [];
|
||||
|
||||
foreach ($observedNetworks as $network) {
|
||||
$bssid = strtolower((string)($network['bssid'] ?? ''));
|
||||
if ($bssid !== '') {
|
||||
$merged[$bssid] = $network;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($freshNetworks as $network) {
|
||||
$bssid = strtolower((string)($network['bssid'] ?? ''));
|
||||
if ($bssid === '') {
|
||||
continue;
|
||||
}
|
||||
$network['observed_source'] = 'live';
|
||||
$network['last_seen_at'] = date('Y-m-d H:i:s');
|
||||
$network['observed_age_seconds'] = 0;
|
||||
$merged[$bssid] = $network;
|
||||
}
|
||||
|
||||
$rows = array_values($merged);
|
||||
usort($rows, static function (array $a, array $b): int {
|
||||
$bandOrder = ['2.4G' => 0, '5G' => 1, '6G' => 2, '기타' => 3];
|
||||
$bandCompare = ($bandOrder[$a['band'] ?? '기타'] ?? 9) <=> ($bandOrder[$b['band'] ?? '기타'] ?? 9);
|
||||
if ($bandCompare !== 0) {
|
||||
return $bandCompare;
|
||||
}
|
||||
$sourceCompare = (($a['observed_source'] ?? '') === 'live' ? 0 : 1) <=> (($b['observed_source'] ?? '') === 'live' ? 0 : 1);
|
||||
if ($sourceCompare !== 0) {
|
||||
return $sourceCompare;
|
||||
}
|
||||
|
||||
return (float)($b['signal_dbm'] ?? -999) <=> (float)($a['signal_dbm'] ?? -999);
|
||||
});
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
function wifi_scan_data(): array
|
||||
{
|
||||
if (!(bool)setting_value('wifi.scan_enabled')) {
|
||||
@@ -2868,7 +3033,7 @@ function wifi_scan_data(): array
|
||||
'cached' => false,
|
||||
'generated_at' => null,
|
||||
'interfaces' => [],
|
||||
'networks' => [],
|
||||
'networks' => wifi_scan_observed_networks((int)setting_value('wifi.scan_observed_hours'), (int)setting_value('wifi.scan_max_networks')),
|
||||
'errors' => [],
|
||||
];
|
||||
}
|
||||
@@ -2908,24 +3073,19 @@ function wifi_scan_data(): array
|
||||
}
|
||||
}
|
||||
|
||||
usort($networks, static function (array $a, array $b): int {
|
||||
$bandOrder = ['2.4G' => 0, '5G' => 1, '6G' => 2, '기타' => 3];
|
||||
$bandCompare = ($bandOrder[$a['band'] ?? '기타'] ?? 9) <=> ($bandOrder[$b['band'] ?? '기타'] ?? 9);
|
||||
if ($bandCompare !== 0) {
|
||||
return $bandCompare;
|
||||
}
|
||||
|
||||
return (float)($b['signal_dbm'] ?? -999) <=> (float)($a['signal_dbm'] ?? -999);
|
||||
});
|
||||
|
||||
$max = (int)setting_value('wifi.scan_max_networks');
|
||||
save_wifi_scan_observations($networks);
|
||||
$observedNetworks = wifi_scan_observed_networks((int)setting_value('wifi.scan_observed_hours'), $max);
|
||||
$mergedNetworks = merge_wifi_scan_networks($networks, $observedNetworks);
|
||||
$payload = [
|
||||
'enabled' => true,
|
||||
'cached' => false,
|
||||
'generated_at' => date('Y-m-d H:i:s'),
|
||||
'interfaces' => array_values($interfaces),
|
||||
'networks' => array_slice($networks, 0, $max),
|
||||
'total_found' => count($networks),
|
||||
'networks' => array_slice($mergedNetworks, 0, $max),
|
||||
'live_found' => count($networks),
|
||||
'total_found' => count($mergedNetworks),
|
||||
'observed_hours' => (int)setting_value('wifi.scan_observed_hours'),
|
||||
'errors' => $errors,
|
||||
];
|
||||
|
||||
|
||||
+45
-6
@@ -151,6 +151,8 @@
|
||||
wifiScanEmpty: 'No nearby WiFi scan results.',
|
||||
wifiScanCached: 'cached',
|
||||
wifiScanFresh: 'fresh',
|
||||
wifiScanDisabledWithHistory: 'scan disabled, showing last observed networks',
|
||||
observed: 'Observed',
|
||||
ssid: 'SSID',
|
||||
channel: 'Channel',
|
||||
security: 'Security',
|
||||
@@ -301,6 +303,8 @@
|
||||
wifiScanEmpty: '주변 WiFi 스캔 결과가 없습니다.',
|
||||
wifiScanCached: '캐시',
|
||||
wifiScanFresh: '실시간',
|
||||
wifiScanDisabledWithHistory: '스캔 꺼짐, 마지막 관측 목록 표시',
|
||||
observed: '관측',
|
||||
ssid: 'SSID',
|
||||
channel: '채널',
|
||||
security: '보안',
|
||||
@@ -1301,6 +1305,8 @@
|
||||
function renderWifi(data) {
|
||||
const rows = data.wifi?.clients || [];
|
||||
if (!els.wifiTable) return;
|
||||
const tableWrap = els.wifiTable.closest('.table-wrap');
|
||||
const scrollLeft = tableWrap ? tableWrap.scrollLeft : 0;
|
||||
els.wifiTable.innerHTML = rows.length
|
||||
? rows.map(row => `
|
||||
<tr>
|
||||
@@ -1316,28 +1322,40 @@
|
||||
</tr>
|
||||
`).join('')
|
||||
: `<tr><td colspan="9">${escapeHtml(t('noWifiClients'))}</td></tr>`;
|
||||
if (tableWrap) {
|
||||
tableWrap.scrollLeft = scrollLeft;
|
||||
}
|
||||
|
||||
renderWifiScan(data.wifi?.scan || null);
|
||||
}
|
||||
|
||||
function renderWifiScan(scan) {
|
||||
if (!els.wifiScan) return;
|
||||
if (!scan || scan.enabled === false) {
|
||||
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="wifi-scan-empty">${escapeHtml(t('wifiScanDisabled'))}</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const networks = Array.isArray(scan.networks) ? scan.networks : [];
|
||||
const errors = Array.isArray(scan.errors) ? scan.errors : [];
|
||||
const status = `${scan.cached ? t('wifiScanCached') : t('wifiScanFresh')} · ${escapeHtml(scan.generated_at || '-')}`;
|
||||
const status = scan.enabled === false
|
||||
? `${t('wifiScanDisabledWithHistory')} · ${scan.observed_hours || '-'}h`
|
||||
: `${scan.cached ? t('wifiScanCached') : t('wifiScanFresh')} · ${escapeHtml(scan.generated_at || '-')}`;
|
||||
const groups = ['2.4G', '5G'].map(band => {
|
||||
const bandRows = networks.filter(row => row.band === band);
|
||||
return `
|
||||
<section class="wifi-scan-band">
|
||||
<section class="wifi-scan-band" data-band="${attr(band)}">
|
||||
<h3>${escapeHtml(band)} <span>${bandRows.length}${scan.total_found ? ` / ${scan.total_found}` : ''}</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('lastSeen'))}</th><th>WiFi</th><th>폭</th><th>속도</th><th>기능</th></tr></thead>
|
||||
<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);
|
||||
@@ -1347,7 +1365,7 @@
|
||||
<td>${escapeHtml(row.channel ?? '-')} ${row.freq ? `(${escapeHtml(row.freq)}MHz)` : ''}</td>
|
||||
<td><span class="wifi-signal ${wifiSignalClass(dbm)}">${escapeHtml(row.signal || '-')}</span></td>
|
||||
<td>${escapeHtml(row.security || '-')}</td>
|
||||
<td>${row.last_seen_ms === null || row.last_seen_ms === undefined ? '-' : `${escapeHtml(row.last_seen_ms)}ms`}</td>
|
||||
<td>${escapeHtml(wifiObservedText(row))}</td>
|
||||
<td>${escapeHtml(wifiStandardText(row))}</td>
|
||||
<td>${escapeHtml(row.channel_width || '-')}</td>
|
||||
<td>${escapeHtml(row.rates || '-')}</td>
|
||||
@@ -1373,6 +1391,27 @@
|
||||
${errorHtml}
|
||||
<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 wifiObservedText(row) {
|
||||
if (row?.observed_source === 'live') {
|
||||
return row.last_seen_ms === null || row.last_seen_ms === undefined
|
||||
? '방금'
|
||||
: `방금 (${row.last_seen_ms}ms)`;
|
||||
}
|
||||
if (row?.observed_age_seconds !== null && row?.observed_age_seconds !== undefined) {
|
||||
return `${timeAgo(row.observed_age_seconds)} · ${row.last_seen_at || '-'}`;
|
||||
}
|
||||
return row?.last_seen_at || '-';
|
||||
}
|
||||
|
||||
function wifiStandardText(row) {
|
||||
|
||||
+1
-1
@@ -373,7 +373,7 @@ html[data-theme="light"] .details-panel{background:#edf3fa;border-color:#b8c7da}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/assets/app.js?v=20260626_settings_mobile1"></script>
|
||||
<script src="/assets/app.js?v=20260626_wifi_observe_scroll1"></script>
|
||||
<script src="/assets/wakelock.js?v=20260607_prefs1"></script>
|
||||
<?php endif; ?>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user