Add LAN clients to dashboard

This commit is contained in:
seo
2026-07-20 22:32:52 +09:00
parent c7ab16642e
commit 7425148af8
4 changed files with 94 additions and 5 deletions
+4 -4
View File
@@ -17,7 +17,7 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를
- RP1 온도, 팬 효율, CPU 전력 상세 차트는 접힘 영역에서 확인 - RP1 온도, 팬 효율, CPU 전력 상세 차트는 접힘 영역에서 확인
- UPS/배터리 잔여 시간 기능은 장치 제거 후 비활성 상태로 두고, 배터리 센서 조회와 장기 학습 계산은 돌리지 않음 - UPS/배터리 잔여 시간 기능은 장치 제거 후 비활성 상태로 두고, 배터리 센서 조회와 장기 학습 계산은 돌리지 않음
- 라즈베리파이 저전압/스로틀링 현재 상태, 복구 중/반복 발생 판정, 부팅 후 이력, 최근 감지 시각, 지속시간, 최근 10분 통계 표시 - 라즈베리파이 저전압/스로틀링 현재 상태, 복구 중/반복 발생 판정, 부팅 후 이력, 최근 감지 시각, 지속시간, 최근 10분 통계 표시
- WiFi client 목록과 2.4G/5G client 수 표시 - WiFi/LAN client 목록과 2.4G/5G/LAN client 수 표시
- 5G 외부 WiFi 모듈이 연결 시간을 제공하지 않는 경우 서버 관측 기반 연결 시간 보정 - 5G 외부 WiFi 모듈이 연결 시간을 제공하지 않는 경우 서버 관측 기반 연결 시간 보정
- WiFi 호스트명이 비어 있으면 MAC 기반 임시명을 표시하고, MAC별 수동 호스트명을 DB에 저장해 이후에도 유지 - WiFi 호스트명이 비어 있으면 MAC 기반 임시명을 표시하고, MAC별 수동 호스트명을 DB에 저장해 이후에도 유지
- WiFi 신호/송신 속도/수신 속도가 비어 있으면 대역, dBm, 송수신 속도, 5G RTL8822BU 특성을 기준으로 결측값 보정 - WiFi 신호/송신 속도/수신 속도가 비어 있으면 대역, dBm, 송수신 속도, 5G RTL8822BU 특성을 기준으로 결측값 보정
@@ -156,7 +156,7 @@ control-wifi-observe.service
3. 대시보드는 `status` snapshot을 렌더링합니다. 3. 대시보드는 `status` snapshot을 렌더링합니다.
4. WebSocket 연결이 성공하면 `status` 메시지로 갱신하고, 실패하면 HTTP fallback을 사용합니다. 4. WebSocket 연결이 성공하면 `status` 메시지로 갱신하고, 실패하면 HTTP fallback을 사용합니다.
5. 팬 조작은 상태 저장, 정책 적용, 로그 저장 순서로 처리합니다. 5. 팬 조작은 상태 저장, 정책 적용, 로그 저장 순서로 처리합니다.
6. WiFi client 목록은 `iw station dump`와 dnsmasq lease를 조합하고, 5G 연결 시간이 `N/A`이면 MAC 기준 최초 감지 시간을 DB에 저장해 경과 시간을 계산합니다. 6. WiFi client 목록은 `iw station dump`와 dnsmasq lease를 조합하고, LAN client는 `bridge fdb show br br0`의 USB LAN 포트 MAC을 dnsmasq lease와 매칭합니다. 5G 연결 시간이 `N/A`이면 MAC 기준 최초 감지 시간을 DB에 저장해 경과 시간을 계산합니다.
7. WiFi hostname이 비어 있으면 MAC 기반 임시명을 표시하고, 수동 저장한 MAC별 이름은 DB에서 우선 적용합니다. 7. WiFi hostname이 비어 있으면 MAC 기반 임시명을 표시하고, 수동 저장한 MAC별 이름은 DB에서 우선 적용합니다.
8. WiFi 신호/송신 속도/수신 속도 값이 비어 있으면 실제로 존재하는 다른 무선 지표와 대역별 기준으로 값을 채웁니다. 8. WiFi 신호/송신 속도/수신 속도 값이 비어 있으면 실제로 존재하는 다른 무선 지표와 대역별 기준으로 값을 채웁니다.
9. `control-wifi-observe.timer`는 사용자 접속과 무관하게 10초마다 5G 관측 세션을 갱신합니다. 9. `control-wifi-observe.timer`는 사용자 접속과 무관하게 10초마다 5G 관측 세션을 갱신합니다.
@@ -233,8 +233,8 @@ control-wifi-observe.service
- WiFi client 표는 갱신 중에도 가로 스크롤 위치를 보존합니다. - WiFi client 표는 갱신 중에도 가로 스크롤 위치를 보존합니다.
- 사용자 서비스 목록은 갱신 중에도 스크롤 위치를 보존하고, 스크롤/터치/휠 조작 직후에는 DOM 교체를 잠시 미룹니다. - 사용자 서비스 목록은 갱신 중에도 스크롤 위치를 보존하고, 스크롤/터치/휠 조작 직후에는 DOM 교체를 잠시 미룹니다.
- 하드웨어 또는 OS 변경 후 fan sysfs 경로를 확인합니다. - 하드웨어 또는 OS 변경 후 fan sysfs 경로를 확인합니다.
- AP 국가는 `PA`로 두고, `router-performance.service``wlan0`, `wlan1` power save를 끄고 출력 요청값을 `3600`으로 넣습니다. 실제 출력은 무선 모듈/드라이버가 허용하는 값에서 멈추며 현재 두 AP 모두 `31.00 dBm`으로 표시됩니다. - AP 국가는 `PA`로 두고, `router-performance.service`WiFi power save를 끄고 출력 요청값을 `3600`으로 넣습니다. 실제 출력은 무선 모듈/드라이버가 허용하는 값에서 멈니다.
- 2.4G `hostapd-24g.conf``max_num_sta=16`로 두어 hostapd station table 제한 가능성을 줄입니다. 실제 동시 접속 수는 내장 `brcmfmac` 펌웨어/드라이버 한계가 따로 걸릴 수 있습니다. - 2.4G `hostapd-24g.conf``max_num_sta=32`로 두어 hostapd station table 제한 가능성을 줄입니다. 실제 동시 접속 수는 내장 `brcmfmac` 펌웨어/드라이버 한계가 따로 걸릴 수 있습니다.
- 2.4G 내장 WiFi는 `hostapd-24g.service` 개별 restart 직후 일부 IoT 단말이 WPA/EAPOL 재협상 루프에 들어갈 수 있으므로, 안정화된 상태에서는 개별 restart를 피하고 필요 시 전체 reboot 또는 채널 변경으로 재초기화합니다. - 2.4G 내장 WiFi는 `hostapd-24g.service` 개별 restart 직후 일부 IoT 단말이 WPA/EAPOL 재협상 루프에 들어갈 수 있으므로, 안정화된 상태에서는 개별 restart를 피하고 필요 시 전체 reboot 또는 채널 변경으로 재초기화합니다.
- `hostapd-24g.service`, `hostapd-5g.service`는 종료 후 해당 `wlan` 인터페이스를 `br0`에서 떼도록 두어, 모듈이 빠졌다가 돌아온 뒤 hostapd가 다시 붙이는 흐름으로 정리합니다. - `hostapd-24g.service`, `hostapd-5g.service`는 종료 후 해당 `wlan` 인터페이스를 `br0`에서 떼도록 두어, 모듈이 빠졌다가 돌아온 뒤 hostapd가 다시 붙이는 흐름으로 정리합니다.
- 5G WiFi 연결 시간은 외부 모듈이 값을 제공하지 않을 때 서버가 처음 감지한 시각 기준으로 계산합니다. 현재 5G 목록에서 MAC이 사라지면 관측 세션을 즉시 종료하므로 재연결 시 0부터 다시 누적됩니다. - 5G WiFi 연결 시간은 외부 모듈이 값을 제공하지 않을 때 서버가 처음 감지한 시각 기준으로 계산합니다. 현재 5G 목록에서 MAC이 사라지면 관측 세션을 즉시 종료하므로 재연결 시 0부터 다시 누적됩니다.
+85 -1
View File
@@ -2604,6 +2604,20 @@ function iw_station_dump(string $iface): string
return sh(['/usr/sbin/iw', 'dev', $iface, 'station', 'dump'], true, 4)['out']; return sh(['/usr/sbin/iw', 'dev', $iface, 'station', 'dump'], true, 4)['out'];
} }
function wifi_iface_by_driver(string $driver): ?string
{
foreach (glob('/sys/class/net/wlan*') ?: [] as $path) {
$iface = basename($path);
$driverPath = @readlink($path . '/device/driver');
if ($driverPath !== false && basename($driverPath) === $driver) {
return $iface;
}
}
return null;
}
function parse_live_wifi_rows(string $iface, string $band, string $text, array $leases, array $aliases): array function parse_live_wifi_rows(string $iface, string $band, string $text, array $leases, array $aliases): array
{ {
$rows = []; $rows = [];
@@ -2678,6 +2692,65 @@ function parse_live_wifi_rows(string $iface, string $band, string $text, array $
return $rows; return $rows;
} }
function lan_fdb_macs(string $iface = 'eth1'): array
{
$rows = sh(['/usr/sbin/bridge', 'fdb', 'show', 'br', 'br0'], false, 4)['out'];
$macs = [];
foreach (explode("\n", $rows) as $line) {
if (!preg_match('/^([0-9a-f]{2}(?::[0-9a-f]{2}){5})\s+dev\s+' . preg_quote($iface, '/') . '\b/i', trim($line), $m)) {
continue;
}
$mac = strtolower($m[1]);
if (str_starts_with($mac, '01:') || str_starts_with($mac, '33:') || str_contains($line, 'permanent')) {
continue;
}
$macs[$mac] = true;
}
return array_keys($macs);
}
function lan_client_rows(array $leases, array $aliases, string $iface = 'eth1'): array
{
$rows = [];
foreach (lan_fdb_macs($iface) as $mac) {
$lease = $leases[$mac] ?? [];
$leaseHostname = (string)($lease['hostname'] ?? 'N/A');
$aliasHostname = $aliases[$mac] ?? null;
$hostname = $aliasHostname ?: $leaseHostname;
$hostnameSource = $aliasHostname ? 'alias' : ($leaseHostname === 'N/A' ? 'generated' : 'lease');
$rows[] = [
'band' => 'LAN',
'iface' => $iface,
'mac' => $mac,
'ip' => $lease['ip'] ?? 'N/A',
'hostname' => $hostname === 'N/A' ? '기기-' . strtoupper(substr(str_replace(':', '', $mac), -6)) : $hostname,
'hostname_source' => $hostnameSource,
'name' => $hostname === 'N/A' ? $mac : $hostname,
'signal' => '-',
'tx_bitrate' => '-',
'rx_bitrate' => '-',
'signal_source' => 'lan',
'tx_bitrate_source' => 'lan',
'rx_bitrate_source' => 'lan',
'connected_time' => 'N/A',
'inactive_time' => 'N/A',
'rx_bytes' => 0,
'tx_bytes' => 0,
'rx_packets' => 0,
'tx_packets' => 0,
'tx_failed' => 0,
];
}
return $rows;
}
function wifi_time_ms(string $value): int function wifi_time_ms(string $value): int
{ {
if (preg_match('/(\d+)/', $value, $m)) { if (preg_match('/(\d+)/', $value, $m)) {
@@ -2762,6 +2835,10 @@ function apply_wifi_estimates(array $clients): array
{ {
foreach ($clients as &$client) { foreach ($clients as &$client) {
$band = (string)($client['band'] ?? '2.4G'); $band = (string)($client['band'] ?? '2.4G');
if ($band === 'LAN') {
continue;
}
$signalDbm = wifi_signal_dbm((string)($client['signal'] ?? '')); $signalDbm = wifi_signal_dbm((string)($client['signal'] ?? ''));
$txMbps = wifi_bitrate_mbps($client['tx_bitrate'] ?? null); $txMbps = wifi_bitrate_mbps($client['tx_bitrate'] ?? null);
$rxMbps = wifi_bitrate_mbps($client['rx_bitrate'] ?? null); $rxMbps = wifi_bitrate_mbps($client['rx_bitrate'] ?? null);
@@ -2973,12 +3050,18 @@ function wifi_data(): array
$aliases = wifi_client_aliases(); $aliases = wifi_client_aliases();
$clients = []; $clients = [];
foreach (['wlan0' => '2.4G', 'wlan1' => '5G'] as $iface => $band) { $wifiIfaces = array_filter([
'wlan0' => '2.4G',
wifi_iface_by_driver('rtl88x2bu') ?: 'wlan1' => '5G',
]);
foreach ($wifiIfaces as $iface => $band) {
$clients = array_merge( $clients = array_merge(
$clients, $clients,
parse_live_wifi_rows($iface, $band, iw_station_dump($iface), $leases, $aliases) parse_live_wifi_rows($iface, $band, iw_station_dump($iface), $leases, $aliases)
); );
} }
$clients = array_merge($clients, lan_client_rows($leases, $aliases));
$clients = dedupe_wifi_clients($clients); $clients = dedupe_wifi_clients($clients);
$clients = apply_wifi_estimates($clients); $clients = apply_wifi_estimates($clients);
@@ -2988,6 +3071,7 @@ function wifi_data(): array
'clients' => $clients, 'clients' => $clients,
'count24' => count(array_filter($clients, fn($c) => $c['band'] === '2.4G')), 'count24' => count(array_filter($clients, fn($c) => $c['band'] === '2.4G')),
'count5' => count(array_filter($clients, fn($c) => $c['band'] === '5G')), 'count5' => count(array_filter($clients, fn($c) => $c['band'] === '5G')),
'countLan' => count(array_filter($clients, fn($c) => $c['band'] === 'LAN')),
'leases' => array_values($leases), 'leases' => array_values($leases),
]; ];
} }
+4
View File
@@ -14,6 +14,7 @@
fanModeOptions: $('#fanModeOptions'), fanModeOptions: $('#fanModeOptions'),
wifi24: $('#wifi24'), wifi24: $('#wifi24'),
wifi5: $('#wifi5'), wifi5: $('#wifi5'),
wifiLan: $('#wifiLan'),
wifiTable: $('#wifiTable'), wifiTable: $('#wifiTable'),
signalTooltip: $('#signalTooltip'), signalTooltip: $('#signalTooltip'),
statusHost: $('#statusHost'), statusHost: $('#statusHost'),
@@ -110,6 +111,7 @@
wifiStatus: 'WiFi Status', wifiStatus: 'WiFi Status',
clients24: '2.4G Clients', clients24: '2.4G Clients',
clients5: '5G Clients', clients5: '5G Clients',
clientsLan: 'LAN Clients',
systemStatus: 'System Status', systemStatus: 'System Status',
loadAvg: 'Load Avg', loadAvg: 'Load Avg',
activeUsers: 'Active Users', activeUsers: 'Active Users',
@@ -257,6 +259,7 @@
wifiStatus: 'WiFi 상태', wifiStatus: 'WiFi 상태',
clients24: '2.4G 접속', clients24: '2.4G 접속',
clients5: '5G 접속', clients5: '5G 접속',
clientsLan: 'LAN 접속',
systemStatus: '시스템 상태', systemStatus: '시스템 상태',
loadAvg: '부하 평균', loadAvg: '부하 평균',
activeUsers: '활성 사용자', activeUsers: '활성 사용자',
@@ -1173,6 +1176,7 @@
setText(els.fanPercent, Number(data.fan?.percent || 0).toFixed(1) + '%'); setText(els.fanPercent, Number(data.fan?.percent || 0).toFixed(1) + '%');
setText(els.wifi24, data.wifi?.count24 ?? 0); setText(els.wifi24, data.wifi?.count24 ?? 0);
setText(els.wifi5, data.wifi?.count5 ?? 0); setText(els.wifi5, data.wifi?.count5 ?? 0);
setText(els.wifiLan, data.wifi?.countLan ?? 0);
state.latestFanPwm = Math.max(0, Math.min(255, Number(data.fan?.pwm || 0))); state.latestFanPwm = Math.max(0, Math.min(255, Number(data.fan?.pwm || 0)));
if (!isFanEditing()) { if (!isFanEditing()) {
+1
View File
@@ -203,6 +203,7 @@ html[data-theme="light"] .details-panel{background:#edf3fa;border-color:#b8c7da}
<div class="stat-grid"> <div class="stat-grid">
<div class="stat"><div class="stat-label" data-i18n="clients24">2.4G Clients</div><div class="stat-value" id="wifi24">-</div></div> <div class="stat"><div class="stat-label" data-i18n="clients24">2.4G Clients</div><div class="stat-value" id="wifi24">-</div></div>
<div class="stat"><div class="stat-label" data-i18n="clients5">5G Clients</div><div class="stat-value" id="wifi5">-</div></div> <div class="stat"><div class="stat-label" data-i18n="clients5">5G Clients</div><div class="stat-value" id="wifi5">-</div></div>
<div class="stat"><div class="stat-label" data-i18n="clientsLan">LAN Clients</div><div class="stat-value" id="wifiLan">-</div></div>
</div> </div>
</div> </div>