주변 와이파이 표기와 스크롤 정리
This commit is contained in:
@@ -21,7 +21,7 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를
|
||||
- 5G 외부 WiFi 모듈이 연결 시간을 제공하지 않는 경우 서버 관측 기반 연결 시간 보정
|
||||
- WiFi 호스트명이 비어 있으면 MAC 기반 임시명을 표시하고, MAC별 수동 호스트명을 DB에 저장해 이후에도 유지
|
||||
- WiFi 신호/송신 속도/수신 속도가 비어 있으면 대역, dBm, 송수신 속도, 5G RTL8822BU 특성을 기준으로 결측값 보정
|
||||
- WiFi client 신호는 dBm이 약할수록 빨간색에 가깝게 표시하고, client 섹션 아래 별도 주변 WiFi 스캔 섹션에서 2.4G/5G AP 관측 결과와 실패 사유를 대역별로 분리 표시
|
||||
- WiFi client 신호는 dBm이 약할수록 빨간색에 가깝게 표시하고, client 섹션 아래 별도 주변 WiFi 스캔 섹션에서 2.4G/5G AP 관측 결과를 대역별로 분리 표시
|
||||
- System Notice 최근 이력 표시
|
||||
- process CPU/MEM 후보는 프로세스 상세 접힘 영역에서 표시
|
||||
- `/etc/systemd/system/*.service` 기준 사용자 생성 서비스의 서비스명, 현재 상태, enable 상태, PID, restart 횟수, 최근 journal 로그를 진단 접힘 영역에서 표시
|
||||
@@ -135,7 +135,7 @@ WebSocket 장기 실행 프로세스에서는 1분 단위 단기 집계 결과
|
||||
- 탭 복귀: 즉시 1회 상태 갱신
|
||||
- 잔여 시간 커스텀 팝오버: 상태 갱신과 동일하며 hover/focus 중에도 내용과 위치를 계속 갱신
|
||||
- dmesg: 진단 패널에서 열었을 때만 1초마다 갱신, 닫으면 중지
|
||||
- 주변 WiFi 스캔: 상태 snapshot에 포함되지만 파일 캐시를 사용하며 기본 60초마다 실제 `iw dev <iface> scan` 명령을 다시 시도, 실패 시 DB의 마지막 성공 관측 목록 표시
|
||||
- 주변 WiFi 스캔: 상태 snapshot에 포함되지만 파일 캐시를 사용하며 기본 60초마다 실제 `iw dev <iface> scan` 명령을 다시 시도, 실패 시 DB의 마지막 성공 관측 목록 표시. 화면의 마지막 관측 경과 문구는 1초 상태 렌더마다 현재 시각 기준으로 다시 계산
|
||||
- WiFi 관측 보정 timer: 10초마다 `bin/wifi_observe.php` 실행
|
||||
- WebSocket 소스 변경 감지: 15초마다 확인 후 재시작
|
||||
- 팬 슬라이더 자동 적용 debounce: 약 450ms
|
||||
@@ -195,9 +195,11 @@ control-wifi-observe.service
|
||||
|
||||
신호/송신 속도/수신 속도는 실제 `iw station dump` 값이 있으면 그대로 표시합니다. 값이 비어 있으면 2.4G는 일반 802.11n 계열 링크 속도와 dBm 범위를 기준으로, 5G는 Realtek RTL8822BU/802.11ac 계열 동글의 최대 1300Mbps 특성을 참고해 dBm과 링크 속도 사이를 보정합니다. 화면에는 별도 표시 없이 숫자만 보여 N/A 노출을 줄입니다.
|
||||
|
||||
주변 WiFi 스캔은 대시보드 snapshot에 포함되지만 기본 60초 파일 캐시를 사용합니다. `iw dev wlan0 scan`, `iw dev wlan1 scan`을 시도하고 SSID, BSSID, 주파수, 채널, 신호, 보안, 마지막 관측, WiFi 세대, 채널 폭, 지원 속도, capability를 표시합니다. AP 모드 또는 드라이버 제한으로 스캔이 실패하면 해당 인터페이스와 실패 사유를 같은 섹션에 표시합니다. 화면은 WiFi client 섹션 아래 별도 카드로 분리하며, 2.4G와 5G를 나란히 두지 않고 각각 독립 표로 세로 배치합니다.
|
||||
주변 WiFi 스캔은 대시보드 snapshot에 포함되지만 기본 60초 파일 캐시를 사용합니다. `iw dev wlan0 scan`, `iw dev wlan1 scan`을 시도하고 SSID, BSSID, 주파수, 채널, 신호, 보안, 마지막 관측, WiFi 세대, 채널 폭, 지원 속도, capability를 표시합니다. 화면은 WiFi client 섹션 아래 별도 카드로 분리하며, 2.4G와 5G를 나란히 두지 않고 각각 독립 표로 세로 배치합니다.
|
||||
|
||||
스캔에 성공한 AP는 `wifi_scan_observations`에 BSSID 기준으로 저장합니다. 다음 스캔이 실패하거나 특정 대역이 AP 모드라 스캔할 수 없어도 목록을 비우지 않고, DB에 누적된 BSSID별 마지막 성공 관측값을 계속 표시합니다. live 스캔 결과는 `방금`으로 표시하고, DB 관측값은 마지막 관측 시각과 경과 시간을 함께 표시합니다.
|
||||
스캔에 성공한 AP는 `wifi_scan_observations`에 BSSID 기준으로 저장합니다. 다음 스캔이 실패하거나 특정 대역이 AP 모드라 스캔할 수 없어도 목록을 비우지 않고, DB에 누적된 BSSID별 마지막 성공 관측값을 계속 표시합니다. 화면의 마지막 관측 경과는 캐시된 API 값에 고정하지 않고 1초 상태 렌더마다 현재 시각 기준으로 다시 계산합니다. live 스캔 결과와 DB 관측값 모두 마지막 관측 시각과 경과 시간을 함께 표시합니다.
|
||||
|
||||
스캔 실패 사유는 API payload에는 남지만 대시보드에는 노란 경고 박스로 표시하지 않습니다. 스캔 표의 보안, 속도, capability 같은 긴 문자열은 말줄임으로 자르지 않고 표 전체를 가로 스크롤해 확인합니다. 대역 제목 옆 숫자는 해당 대역에 표시 중인 AP 개수입니다.
|
||||
|
||||
## 처리 흐름
|
||||
|
||||
@@ -208,7 +210,7 @@ control-wifi-observe.service
|
||||
5. WiFi client 목록은 `iw station dump`와 dnsmasq lease를 조합하고, 5G 연결 시간이 `N/A`이면 MAC 기준 최초 감지 시간을 DB에 저장해 경과 시간을 계산합니다.
|
||||
6. WiFi hostname이 비어 있으면 MAC 기반 임시명을 표시하고, 수동 저장한 MAC별 이름은 DB에서 우선 적용합니다.
|
||||
7. WiFi 신호/송신 속도/수신 속도 값이 비어 있으면 실제로 존재하는 다른 무선 지표와 대역별 기준으로 값을 채웁니다.
|
||||
8. 주변 WiFi 스캔은 캐시 주기를 확인한 뒤 `iw dev <iface> scan`을 시도하고, 성공 결과를 DB에 저장한 뒤 live 값과 마지막 관측값을 client 표 아래에 표시합니다.
|
||||
8. 주변 WiFi 스캔은 캐시 주기를 확인한 뒤 `iw dev <iface> scan`을 시도하고, 성공 결과를 DB에 저장한 뒤 live 값과 마지막 관측값을 주변 WiFi 스캔 전용 카드에 표시합니다.
|
||||
9. `control-wifi-observe.timer`는 사용자 접속과 무관하게 10초마다 5G 관측 세션을 갱신합니다.
|
||||
10. WakeLock 버튼은 활성 상태를 초록색 버튼으로 표시합니다.
|
||||
11. Reboot 버튼은 설정된 확인 문구와 관리자 암호 재입력을 모두 통과한 뒤 서버 API로 재부팅을 요청합니다.
|
||||
|
||||
+15
-13
@@ -1346,7 +1346,6 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const errors = Array.isArray(scan.errors) ? scan.errors : [];
|
||||
const observedMode = scan.observed_mode === 'all' ? t('wifiScanObservedAll') : `${scan.observed_hours || '-'}h`;
|
||||
const status = scan.enabled === false
|
||||
? `${t('wifiScanDisabledWithHistory')} · ${observedMode}`
|
||||
@@ -1361,7 +1360,7 @@
|
||||
const bandRows = networks.filter(row => row.band === band);
|
||||
return `
|
||||
<section class="wifi-scan-band" data-band="${attr(band)}">
|
||||
<h3>${escapeHtml(band)} <span>${bandRows.length}${scan.total_found ? ` / ${scan.total_found}` : ''}</span></h3>
|
||||
<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>
|
||||
@@ -1388,16 +1387,10 @@
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
const errorHtml = errors.length
|
||||
? `<div class="wifi-scan-errors">${errors.map(row => `<div><strong>${escapeHtml(row.band || row.iface || '-')}</strong> ${escapeHtml(row.reason || '-')}</div>`).join('')}</div>`
|
||||
: '';
|
||||
|
||||
els.wifiScan.innerHTML = `
|
||||
<div class="wifi-scan-head">
|
||||
<h3>${escapeHtml(t('wifiScan'))}</h3>
|
||||
<span>${status}</span>
|
||||
</div>
|
||||
${errorHtml}
|
||||
<div class="wifi-scan-grid">${groups}</div>
|
||||
`;
|
||||
requestAnimationFrame(() => {
|
||||
@@ -1413,14 +1406,23 @@
|
||||
|
||||
function wifiObservedText(row) {
|
||||
if (row?.observed_source === 'live') {
|
||||
return row.last_seen_ms === null || row.last_seen_ms === undefined
|
||||
? '방금'
|
||||
: `방금 (${row.last_seen_ms}ms)`;
|
||||
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)} · ${row.last_seen_at || '-'}`;
|
||||
return `${timeAgo(row.observed_age_seconds)} · -`;
|
||||
}
|
||||
return row?.last_seen_at || '-';
|
||||
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) {
|
||||
|
||||
+2
-2
@@ -127,7 +127,7 @@ html[data-theme="light"] .btn.secondary{background:#d9e2ef;color:#172033}html[da
|
||||
.status-list{display:grid;gap:10px}.status-row{display:grid;grid-template-columns:112px minmax(0,1fr);gap:12px;align-items:baseline;padding:10px 12px;background:var(--card2);border-radius:14px}.status-key{color:var(--sub);font-size:13px}.status-value{overflow:visible;white-space:normal;word-break:break-word;font-variant-numeric:tabular-nums}
|
||||
.table-wrap{max-width:100%;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;border-radius:16px;border:1px solid var(--line)}.table-wrap.compact{border-radius:12px}.wifi-table{width:100%;min-width:1080px;table-layout:fixed;border-collapse:collapse}.wifi-table th,.wifi-table td{padding:11px 13px;border-bottom:1px solid rgba(128,145,170,.18);text-align:left;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.wifi-table th{position:sticky;top:0;background:var(--table-head);z-index:1;font-weight:500}.wifi-table tr:hover{background:rgba(128,145,170,.09)}.wifi-table .col-band{width:72px}.wifi-table .col-host{width:240px}.wifi-table .col-ip{width:132px}.wifi-table .col-mac{width:170px}.wifi-table .col-signal{width:100px}.wifi-table .col-rate{width:120px}.wifi-table .col-time{width:130px}
|
||||
.wifi-host-edit{appearance:none;border:0;background:transparent;color:inherit;font:inherit;max-width:100%;padding:0;margin:0;cursor:pointer;text-align:left;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-decoration:underline;text-decoration-style:dotted;text-underline-offset:3px}.wifi-host-edit:hover{color:var(--blue)}
|
||||
.wifi-signal{font-weight:700;font-variant-numeric:tabular-nums}.wifi-signal.excellent{color:#22c55e}.wifi-signal.good{color:#38bdf8}.wifi-signal.fair{color:#f59e0b}.wifi-signal.weak{color:#fb7185}.wifi-signal.bad{color:#ef4444}.wifi-signal.unknown{color:var(--sub)}.wifi-scan{display:grid;gap:12px}.wifi-scan-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px}.wifi-scan-head h3{margin:0;color:var(--text);font-size:15px;font-weight:600}.wifi-scan-head span{color:var(--sub);font-size:12px;font-variant-numeric:tabular-nums}.wifi-scan-grid{display:grid;grid-template-columns:1fr;gap:14px}.wifi-scan-band{min-width:0;display:grid;gap:8px}.wifi-scan-band h3{margin:0;color:var(--sub);font-size:13px;font-weight:600}.wifi-scan-band h3 span{font-weight:500;color:var(--sub)}.wifi-scan-table{width:100%;min-width:1180px;table-layout:fixed;border-collapse:collapse}.wifi-scan-table th,.wifi-scan-table td{padding:9px 10px;border-bottom:1px solid rgba(128,145,170,.16);font-size:12px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.wifi-scan-table th{background:var(--table-head);color:var(--sub);font-weight:500}.wifi-scan-table th:nth-child(1),.wifi-scan-table td:nth-child(1){width:190px}.wifi-scan-table th:nth-child(2),.wifi-scan-table td:nth-child(2){width:145px}.wifi-scan-table th:nth-child(3),.wifi-scan-table td:nth-child(3){width:112px}.wifi-scan-table th:nth-child(4),.wifi-scan-table td:nth-child(4){width:92px}.wifi-scan-table th:nth-child(5),.wifi-scan-table td:nth-child(5){width:210px}.wifi-scan-table th:nth-child(6),.wifi-scan-table td:nth-child(6){width:150px}.wifi-scan-table th:nth-child(7),.wifi-scan-table td:nth-child(7){width:105px}.wifi-scan-table th:nth-child(8),.wifi-scan-table td:nth-child(8){width:90px}.wifi-scan-table th:nth-child(9),.wifi-scan-table td:nth-child(9){width:150px}.wifi-scan-errors,.wifi-scan-empty{border:1px solid rgba(245,158,11,.35);background:rgba(245,158,11,.1);color:var(--text);border-radius:12px;padding:10px 12px;font-size:13px;line-height:1.5}.wifi-scan-errors{display:grid;gap:5px}.wifi-scan-errors strong{color:#fbbf24;margin-right:5px}
|
||||
.wifi-signal{font-weight:700;font-variant-numeric:tabular-nums}.wifi-signal.excellent{color:#22c55e}.wifi-signal.good{color:#38bdf8}.wifi-signal.fair{color:#f59e0b}.wifi-signal.weak{color:#fb7185}.wifi-signal.bad{color:#ef4444}.wifi-signal.unknown{color:var(--sub)}.wifi-scan{display:grid;gap:12px}.wifi-scan-head{display:flex;align-items:baseline;justify-content:flex-end;gap:12px}.wifi-scan-head span{color:var(--sub);font-size:12px;font-variant-numeric:tabular-nums}.wifi-scan-grid{display:grid;grid-template-columns:1fr;gap:14px}.wifi-scan-band{min-width:0;display:grid;gap:8px}.wifi-scan-band h3{margin:0;color:var(--sub);font-size:13px;font-weight:600}.wifi-scan-band h3 span{font-weight:500;color:var(--sub)}.wifi-scan-table{width:max-content;min-width:100%;table-layout:auto;border-collapse:collapse}.wifi-scan-table th,.wifi-scan-table td{width:auto!important;max-width:none!important;padding:9px 10px;border-bottom:1px solid rgba(128,145,170,.16);font-size:12px;text-align:left;white-space:nowrap;overflow:visible;text-overflow:clip}.wifi-scan-table th{background:var(--table-head);color:var(--sub);font-weight:500}.wifi-scan-errors,.wifi-scan-empty{border:1px solid rgba(245,158,11,.35);background:rgba(245,158,11,.1);color:var(--text);border-radius:12px;padding:10px 12px;font-size:13px;line-height:1.5}
|
||||
.chart-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px}.chart-box{height:280px;background:var(--input);border:1px solid rgba(84,101,128,.58);border-radius:16px;padding:12px 12px 18px}.chart-box h3{margin:0 0 8px;color:var(--sub);font-size:13px}.chart-canvas{position:relative;height:calc(100% - 27px);min-height:0}.chart-canvas canvas{width:100%!important;height:100%!important}.chart-secondary{margin-top:14px}
|
||||
.details-panel{border:1px solid rgba(84,101,128,.52);border-radius:16px;background:rgba(128,145,170,.06);padding:0;margin-top:14px;overflow:hidden}.details-panel>summary{cursor:pointer;list-style:none;display:flex;align-items:center;justify-content:space-between;gap:12px;min-height:48px;padding:0 14px;color:var(--text);font-weight:500}.details-panel>summary::-webkit-details-marker{display:none}.details-panel>summary::after{content:"+";display:grid;place-items:center;width:24px;height:24px;border:1px solid var(--line);border-radius:999px;color:var(--sub);font-weight:700}.details-panel[open]>summary::after{content:"-"}.details-body{padding:0 14px 14px}.section-note{margin:0;color:var(--sub);font-size:12px}
|
||||
.resource-card{margin-top:18px}.resource-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:12px}.resource-head h2{margin:0}.baseline-pill{color:var(--sub);font-size:12px;font-variant-numeric:tabular-nums;text-align:right;white-space:nowrap}.spike-log-box{margin-top:16px}.spike-log-box h3{margin:0 0 9px;color:var(--text);font-size:14px;font-weight:500}.spike-log-list{display:grid;gap:8px;max-height:260px;overflow:auto;padding-right:4px}.spike-log-list.compact{max-height:180px}.spike-log-item{display:grid;gap:4px;padding:10px 12px;border:1px solid var(--notice-border);background:var(--notice-bg);border-radius:12px}.spike-log-item.latest{border:1px solid var(--notice-border-strong);background:var(--notice-bg-strong);box-shadow:0 0 0 1px var(--notice-border) inset;}.spike-log-item.alert{border-color:rgba(255,95,87,.58);background:rgba(150,38,38,.18)}.spike-log-item.alert strong{color:#ffb4ad}.spike-log-item strong{font-size:13px;font-weight:500;color:var(--notice-title)}.spike-log-item span{font-size:12px;color:var(--notice-copy);font-variant-numeric:tabular-nums}.spike-log-empty{padding:10px 12px;border-radius:12px;background:var(--card2);color:var(--sub);font-size:13px}.service-list{display:grid;gap:10px;max-height:520px;overflow:auto;padding-right:4px}.service-item{border:1px solid var(--line);border-radius:12px;background:var(--card2);padding:12px;display:grid;gap:9px}.service-head{display:flex;justify-content:space-between;gap:10px;align-items:flex-start}.service-title{display:grid;gap:3px;min-width:0}.service-name{font-size:14px;color:var(--text);font-weight:600;word-break:break-word}.service-desc,.service-meta{font-size:12px;color:var(--sub);font-variant-numeric:tabular-nums;word-break:break-word}.service-state{border:1px solid var(--line);border-radius:999px;padding:4px 9px;font-size:12px;color:var(--sub);white-space:nowrap}.service-state.active{color:#bbf7d0;border-color:rgba(34,197,94,.35);background:rgba(22,163,74,.14)}.service-state.failed{color:#fecaca;border-color:rgba(239,68,68,.4);background:rgba(185,28,28,.16)}.service-log{margin:0;max-height:160px;overflow:auto;border:1px solid var(--line);border-radius:10px;background:var(--code-bg);color:var(--mono-text);padding:10px;font:11px/1.45 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;white-space:pre-wrap;word-break:break-word}.dmesg-head{display:flex;align-items:center;justify-content:space-between;gap:10px}.dmesg-meta{font-size:12px;color:var(--sub);font-variant-numeric:tabular-nums}.dmesg-log{margin:10px 0 0;max-height:420px;overflow:auto;border:1px solid var(--line);border-radius:12px;background:var(--code-bg);color:var(--mono-text);padding:12px;font:12px/1.45 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;white-space:pre-wrap;word-break:break-word}.dmesg-log[hidden]{display:none}
|
||||
@@ -377,7 +377,7 @@ html[data-theme="light"] .details-panel{background:#edf3fa;border-color:#b8c7da}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/assets/app.js?v=20260627_wifi_scan_split1"></script>
|
||||
<script src="/assets/app.js?v=20260627_wifi_scan_readable1"></script>
|
||||
<script src="/assets/wakelock.js?v=20260607_prefs1"></script>
|
||||
<?php endif; ?>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user