스캔 표 스크롤과 신호 표시를 다듬음
This commit is contained in:
@@ -21,7 +21,7 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를
|
|||||||
- 5G 외부 WiFi 모듈이 연결 시간을 제공하지 않는 경우 서버 관측 기반 연결 시간 보정
|
- 5G 외부 WiFi 모듈이 연결 시간을 제공하지 않는 경우 서버 관측 기반 연결 시간 보정
|
||||||
- WiFi 호스트명이 비어 있으면 MAC 기반 임시명을 표시하고, MAC별 수동 호스트명을 DB에 저장해 이후에도 유지
|
- WiFi 호스트명이 비어 있으면 MAC 기반 임시명을 표시하고, MAC별 수동 호스트명을 DB에 저장해 이후에도 유지
|
||||||
- WiFi 신호/송신 속도/수신 속도가 비어 있으면 대역, dBm, 송수신 속도, 5G RTL8822BU 특성을 기준으로 결측값 보정
|
- WiFi 신호/송신 속도/수신 속도가 비어 있으면 대역, dBm, 송수신 속도, 5G RTL8822BU 특성을 기준으로 결측값 보정
|
||||||
- WiFi client 신호는 dBm이 약할수록 빨간색에 가깝게 표시하고, client 섹션 아래 별도 주변 WiFi/Bluetooth 스캔 섹션에서 마지막 관측 결과를 계속 표시
|
- WiFi client, 주변 WiFi 스캔, 주변 Bluetooth 스캔의 신호는 7단계 한글 등급으로 표시하고, hover/focus/touch 시 커스텀 툴팁에서 실제 dBm을 표시
|
||||||
- System Notice 최근 이력 표시
|
- System Notice 최근 이력 표시
|
||||||
- process CPU/MEM 후보는 프로세스 상세 접힘 영역에서 표시
|
- process CPU/MEM 후보는 프로세스 상세 접힘 영역에서 표시
|
||||||
- `/etc/systemd/system/*.service` 기준 사용자 생성 서비스의 서비스명, 현재 상태, enable 상태, PID, restart 횟수, 최근 journal 로그를 진단 접힘 영역에서 표시
|
- `/etc/systemd/system/*.service` 기준 사용자 생성 서비스의 서비스명, 현재 상태, enable 상태, PID, restart 횟수, 최근 journal 로그를 진단 접힘 영역에서 표시
|
||||||
@@ -195,13 +195,13 @@ control-wifi-observe.service
|
|||||||
|
|
||||||
호스트명은 dnsmasq lease 값을 우선 사용합니다. lease hostname이 `N/A`이면 MAC 주소 끝 6자리 기반 임시명 `기기-XXXXXX`을 표시하고, 표의 호스트명을 클릭해 저장한 수동 이름은 `wifi_client_aliases`에 보관해 이후에도 우선 표시합니다.
|
호스트명은 dnsmasq lease 값을 우선 사용합니다. lease hostname이 `N/A`이면 MAC 주소 끝 6자리 기반 임시명 `기기-XXXXXX`을 표시하고, 표의 호스트명을 클릭해 저장한 수동 이름은 `wifi_client_aliases`에 보관해 이후에도 우선 표시합니다.
|
||||||
|
|
||||||
신호/송신 속도/수신 속도는 실제 `iw station dump` 값이 있으면 그대로 표시합니다. 값이 비어 있으면 2.4G는 일반 802.11n 계열 링크 속도와 dBm 범위를 기준으로, 5G는 Realtek RTL8822BU/802.11ac 계열 동글의 최대 1300Mbps 특성을 참고해 dBm과 링크 속도 사이를 보정합니다. 화면에는 별도 표시 없이 숫자만 보여 N/A 노출을 줄입니다.
|
신호/송신 속도/수신 속도는 실제 `iw station dump` 값이 있으면 그대로 사용합니다. 값이 비어 있으면 2.4G는 일반 802.11n 계열 링크 속도와 dBm 범위를 기준으로, 5G는 Realtek RTL8822BU/802.11ac 계열 동글의 최대 1300Mbps 특성을 참고해 dBm과 링크 속도 사이를 보정합니다. 화면의 신호 칸은 숫자 대신 `최상`, `매우 좋음`, `좋음`, `양호`, `보통`, `약함`, `매우 약함` 7단계 등급으로 표시하고, 마우스 hover/키보드 focus/모바일 touch 시 커스텀 툴팁으로 실제 dBm을 표시합니다. 툴팁 내용과 위치는 1초 상태 렌더마다 다시 갱신됩니다.
|
||||||
|
|
||||||
주변 WiFi 스캔은 대시보드 snapshot에 포함되지만 기본 60초 파일 캐시를 사용합니다. `iw dev wlan0 scan`, `iw dev wlan1 scan`을 시도하고 SSID, BSSID, 주파수, 채널, 신호, 보안, 마지막 관측, WiFi 세대, 채널 폭, 지원 속도, capability를 표시합니다. 화면은 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 기준으로 저장합니다. SSID, BSSID, 신호, 보안, 채널 폭 같은 화면 표시값뿐 아니라 `iw` 원문과 파싱 결과 JSON도 함께 저장해 이후 분석 근거를 남깁니다. 다음 스캔이 실패하거나 특정 대역이 AP 모드라 스캔할 수 없어도 목록을 비우지 않고, DB에 누적된 BSSID별 마지막 성공 관측값을 계속 표시합니다. 화면의 마지막 관측 경과는 캐시된 API 값에 고정하지 않고 1초 상태 렌더마다 현재 시각 기준으로 다시 계산합니다. live 스캔 결과와 DB 관측값 모두 마지막 관측 시각과 경과 시간을 함께 표시합니다.
|
스캔에 성공한 AP는 `wifi_scan_observations`에 BSSID 기준으로 저장합니다. SSID, BSSID, 신호, 보안, 채널 폭 같은 화면 표시값뿐 아니라 `iw` 원문과 파싱 결과 JSON도 함께 저장해 이후 분석 근거를 남깁니다. 다음 스캔이 실패하거나 특정 대역이 AP 모드라 스캔할 수 없어도 목록을 비우지 않고, DB에 누적된 BSSID별 마지막 성공 관측값을 계속 표시합니다. 화면의 마지막 관측 경과는 캐시된 API 값에 고정하지 않고 1초 상태 렌더마다 현재 시각 기준으로 다시 계산합니다. live 스캔 결과와 DB 관측값 모두 마지막 관측 시각과 경과 시간을 함께 표시합니다.
|
||||||
|
|
||||||
스캔 실패 사유는 API payload에는 남지만 대시보드에는 노란 경고 박스로 표시하지 않습니다. 스캔 표의 보안, 속도, capability 같은 긴 문자열은 말줄임으로 자르지 않고 표 전체를 가로 스크롤해 확인합니다. 대역 제목 옆 숫자는 해당 대역에 표시 중인 AP 개수입니다.
|
스캔 실패 사유는 API payload에는 남지만 대시보드에는 노란 경고 박스로 표시하지 않습니다. 스캔 표의 보안, 속도, capability 같은 긴 문자열은 말줄임으로 자르지 않고 표 전체를 가로 스크롤해 확인합니다. 대역 제목 옆 숫자는 해당 대역에 표시 중인 AP 개수입니다. 스캔 표에서 가로 스크롤을 드래그하거나 스크롤 중일 때는 1초 상태 렌더가 표 컨테이너를 교체하지 않도록 짧게 보류해 스크롤바 클릭/드래그가 끊기지 않게 합니다.
|
||||||
|
|
||||||
주변 Bluetooth 스캔은 WiFi 스캔 카드 아래 별도 카드로 표시합니다. `bluetoothctl scan on`, `bluetoothctl devices`, `bluetoothctl info <address>`를 조합해 주소, 이름, alias, RSSI, TxPower, 연결/페어링/신뢰/차단 상태, class, icon, UUID, 제조사 데이터, 서비스 데이터, modalias, appearance를 가능한 범위에서 수집합니다. 성공한 장치는 `bluetooth_scan_observations`에 주소 기준으로 저장하며 원문과 파싱 결과 JSON도 함께 남깁니다. 스캔이 실패하거나 주변 장치가 일시적으로 사라져도 마지막 성공 관측값을 계속 표시하고, 마지막 관측 경과는 1초 상태 렌더마다 다시 계산합니다.
|
주변 Bluetooth 스캔은 WiFi 스캔 카드 아래 별도 카드로 표시합니다. `bluetoothctl scan on`, `bluetoothctl devices`, `bluetoothctl info <address>`를 조합해 주소, 이름, alias, RSSI, TxPower, 연결/페어링/신뢰/차단 상태, class, icon, UUID, 제조사 데이터, 서비스 데이터, modalias, appearance를 가능한 범위에서 수집합니다. 성공한 장치는 `bluetooth_scan_observations`에 주소 기준으로 저장하며 원문과 파싱 결과 JSON도 함께 남깁니다. 스캔이 실패하거나 주변 장치가 일시적으로 사라져도 마지막 성공 관측값을 계속 표시하고, 마지막 관측 경과는 1초 상태 렌더마다 다시 계산합니다.
|
||||||
|
|
||||||
@@ -289,7 +289,7 @@ control-wifi-observe.service
|
|||||||
- 센서 수집 주기와 DB 증가량을 확인합니다.
|
- 센서 수집 주기와 DB 증가량을 확인합니다.
|
||||||
- 배터리팩 변경 시 `/home/seo/secret/control.php`의 `battery` 값을 먼저 갱신합니다. 현재 기준은 `2200mAh x 8 parallel = 17600mAh`, nominal `65.12Wh`이며, SOC 추세가 부족한 경우의 fallback 계산에 사용됩니다.
|
- 배터리팩 변경 시 `/home/seo/secret/control.php`의 `battery` 값을 먼저 갱신합니다. 현재 기준은 `2200mAh x 8 parallel = 17600mAh`, nominal `65.12Wh`이며, SOC 추세가 부족한 경우의 fallback 계산에 사용됩니다.
|
||||||
- 잔여 시간이 갑자기 흔들리면 커스텀 팝오버의 `계산 출처`, `반영 구간`, `시간당 SOC 감소`, `추세 신뢰도`, `최근 CPU 전력`을 먼저 확인합니다.
|
- 잔여 시간이 갑자기 흔들리면 커스텀 팝오버의 `계산 출처`, `반영 구간`, `시간당 SOC 감소`, `추세 신뢰도`, `최근 CPU 전력`을 먼저 확인합니다.
|
||||||
- WiFi client 표와 주변 WiFi/Bluetooth 스캔 표는 갱신 중에도 가로 스크롤 위치를 보존합니다. 주변 WiFi 스캔은 별도 카드에서 대역별 독립 표로 렌더링하고, Bluetooth 스캔은 그 아래 독립 카드에서 장치별 상세 표로 렌더링해 client 표 스크롤과 서로 간섭하지 않도록 합니다.
|
- WiFi client 표와 주변 WiFi/Bluetooth 스캔 표는 갱신 중에도 가로 스크롤 위치를 보존합니다. 주변 WiFi 스캔은 별도 카드에서 대역별 독립 표로 렌더링하고, Bluetooth 스캔은 그 아래 독립 카드에서 장치별 상세 표로 렌더링해 client 표 스크롤과 서로 간섭하지 않도록 합니다. 스캔 표는 스크롤/드래그 중 DOM 재생성을 잠시 보류해 사용자의 좌우 스크롤 조작을 우선합니다.
|
||||||
- 하드웨어 또는 OS 변경 후 fan sysfs 경로를 확인합니다.
|
- 하드웨어 또는 OS 변경 후 fan sysfs 경로를 확인합니다.
|
||||||
- 2.4G 내장 WiFi는 `hostapd-24g.service` 개별 restart 직후 일부 IoT 단말이 WPA/EAPOL 재협상 루프에 들어갈 수 있으므로, 안정화된 상태에서는 개별 restart를 피하고 필요 시 전체 reboot 또는 채널 변경으로 재초기화합니다.
|
- 2.4G 내장 WiFi는 `hostapd-24g.service` 개별 restart 직후 일부 IoT 단말이 WPA/EAPOL 재협상 루프에 들어갈 수 있으므로, 안정화된 상태에서는 개별 restart를 피하고 필요 시 전체 reboot 또는 채널 변경으로 재초기화합니다.
|
||||||
- 5G WiFi 연결 시간은 외부 모듈이 값을 제공하지 않을 때 서버가 처음 감지한 시각 기준으로 계산합니다. 현재 5G 목록에서 MAC이 사라지면 관측 세션을 즉시 종료하므로 재연결 시 0부터 다시 누적됩니다.
|
- 5G WiFi 연결 시간은 외부 모듈이 값을 제공하지 않을 때 서버가 처음 감지한 시각 기준으로 계산합니다. 현재 5G 목록에서 MAC이 사라지면 관측 세션을 즉시 종료하므로 재연결 시 0부터 다시 누적됩니다.
|
||||||
|
|||||||
+192
-27
@@ -17,6 +17,7 @@
|
|||||||
wifiTable: $('#wifiTable'),
|
wifiTable: $('#wifiTable'),
|
||||||
wifiScan: $('#wifiScan'),
|
wifiScan: $('#wifiScan'),
|
||||||
bluetoothScan: $('#bluetoothScan'),
|
bluetoothScan: $('#bluetoothScan'),
|
||||||
|
signalTooltip: $('#signalTooltip'),
|
||||||
statusHost: $('#statusHost'),
|
statusHost: $('#statusHost'),
|
||||||
statusLoad: $('#statusLoad'),
|
statusLoad: $('#statusLoad'),
|
||||||
statusUsers: $('#statusUsers'),
|
statusUsers: $('#statusUsers'),
|
||||||
@@ -86,6 +87,11 @@
|
|||||||
theme: 'dark',
|
theme: 'dark',
|
||||||
batteryTooltipOpen: false,
|
batteryTooltipOpen: false,
|
||||||
batteryTooltipText: '',
|
batteryTooltipText: '',
|
||||||
|
signalTooltipOpen: false,
|
||||||
|
signalTooltipTarget: null,
|
||||||
|
signalTooltipKey: '',
|
||||||
|
signalTooltipText: '',
|
||||||
|
scanScrollHoldUntil: 0,
|
||||||
settingsPayload: null,
|
settingsPayload: null,
|
||||||
settingsOpen: false,
|
settingsOpen: false,
|
||||||
settingsDirty: false,
|
settingsDirty: false,
|
||||||
@@ -1108,6 +1114,104 @@
|
|||||||
els.batteryRuntimeTooltip.setAttribute('aria-hidden', 'true');
|
els.batteryRuntimeTooltip.setAttribute('aria-hidden', 'true');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function signalTooltipTarget() {
|
||||||
|
if (state.signalTooltipKey && typeof document.querySelector === 'function') {
|
||||||
|
const selector = `[data-signal-key="${cssEscape(state.signalTooltipKey)}"]`;
|
||||||
|
const fresh = document.querySelector(selector);
|
||||||
|
if (fresh) {
|
||||||
|
state.signalTooltipTarget = fresh;
|
||||||
|
return fresh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return state.signalTooltipTarget?.isConnected ? state.signalTooltipTarget : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function signalTooltipText(target = signalTooltipTarget()) {
|
||||||
|
if (!target) return '-';
|
||||||
|
const label = target.dataset.signalLabel || target.textContent || '-';
|
||||||
|
const dbm = target.dataset.signalDbm || '-';
|
||||||
|
return `${label}\n${dbm}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function positionSignalTooltip() {
|
||||||
|
if (!state.signalTooltipOpen || !els.signalTooltip) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const target = signalTooltipTarget();
|
||||||
|
if (!target) {
|
||||||
|
hideSignalTooltip();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetRect = target.getBoundingClientRect();
|
||||||
|
const tip = els.signalTooltip;
|
||||||
|
const margin = 14;
|
||||||
|
tip.style.left = '0px';
|
||||||
|
tip.style.top = '0px';
|
||||||
|
const tipRect = tip.getBoundingClientRect();
|
||||||
|
let left = targetRect.left + (targetRect.width / 2) - (tipRect.width / 2);
|
||||||
|
let top = targetRect.bottom + 10;
|
||||||
|
|
||||||
|
if (left + tipRect.width > window.innerWidth - margin) {
|
||||||
|
left = window.innerWidth - tipRect.width - margin;
|
||||||
|
}
|
||||||
|
if (left < margin) {
|
||||||
|
left = margin;
|
||||||
|
}
|
||||||
|
if (top + tipRect.height > window.innerHeight - margin) {
|
||||||
|
top = targetRect.top - tipRect.height - 10;
|
||||||
|
}
|
||||||
|
if (top < margin) {
|
||||||
|
top = margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
tip.style.left = `${Math.round(left)}px`;
|
||||||
|
tip.style.top = `${Math.round(top)}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSignalTooltip() {
|
||||||
|
if (!els.signalTooltip) return;
|
||||||
|
state.signalTooltipText = signalTooltipText();
|
||||||
|
els.signalTooltip.textContent = state.signalTooltipText || '-';
|
||||||
|
if (state.signalTooltipOpen) {
|
||||||
|
requestAnimationFrame(positionSignalTooltip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSignalTooltip(target) {
|
||||||
|
if (!els.signalTooltip || !target || target.classList.contains('unknown')) return;
|
||||||
|
state.signalTooltipOpen = true;
|
||||||
|
state.signalTooltipTarget = target;
|
||||||
|
state.signalTooltipKey = target.dataset.signalKey || '';
|
||||||
|
els.signalTooltip.dataset.open = '1';
|
||||||
|
els.signalTooltip.setAttribute('aria-hidden', 'false');
|
||||||
|
updateSignalTooltip();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideSignalTooltip() {
|
||||||
|
if (!els.signalTooltip) return;
|
||||||
|
state.signalTooltipOpen = false;
|
||||||
|
state.signalTooltipTarget = null;
|
||||||
|
state.signalTooltipKey = '';
|
||||||
|
delete els.signalTooltip.dataset.open;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
return String(value).replace(/["\\]/g, '\\$&');
|
||||||
|
}
|
||||||
|
|
||||||
function renderTop(data) {
|
function renderTop(data) {
|
||||||
setText(els.updated, data.generated_at || '-');
|
setText(els.updated, data.generated_at || '-');
|
||||||
setText(els.temp, Number(data.system?.temp_c || 0).toFixed(1) + '°C');
|
setText(els.temp, Number(data.system?.temp_c || 0).toFixed(1) + '°C');
|
||||||
@@ -1159,9 +1263,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const dbm = wifiSignalDbm(row[key]);
|
const dbm = wifiSignalDbm(row[key]);
|
||||||
const cls = wifiSignalClass(dbm);
|
return `<td>${signalBadgeHtml(dbm, row[key] || '', `client:${row.mac || row.ip || row.hostname || ''}`)}</td>`;
|
||||||
const title = dbm === null ? '' : `title="${attr(signalQualityText(dbm))}"`;
|
|
||||||
return `<td><span class="wifi-signal ${cls}" ${title}>${escapeHtml(row[key] || '-')}</span></td>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function wifiSignalDbm(value) {
|
function wifiSignalDbm(value) {
|
||||||
@@ -1169,21 +1271,32 @@
|
|||||||
return match ? Number(match[0]) : null;
|
return match ? Number(match[0]) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function wifiSignalClass(dbm) {
|
function signalQuality(dbm) {
|
||||||
if (dbm === null || !Number.isFinite(dbm)) return 'unknown';
|
if (dbm === null || !Number.isFinite(dbm)) return 'unknown';
|
||||||
if (dbm >= -55) return 'excellent';
|
if (dbm >= -45) return { className: 'best', label: '최상' };
|
||||||
if (dbm >= -67) return 'good';
|
if (dbm >= -55) return { className: 'excellent', label: '매우 좋음' };
|
||||||
if (dbm >= -76) return 'fair';
|
if (dbm >= -65) return { className: 'good', label: '좋음' };
|
||||||
if (dbm >= -84) return 'weak';
|
if (dbm >= -72) return { className: 'fine', label: '양호' };
|
||||||
return 'bad';
|
if (dbm >= -80) return { className: 'normal', label: '보통' };
|
||||||
|
if (dbm >= -88) return { className: 'weak', label: '약함' };
|
||||||
|
return { className: 'bad', label: '매우 약함' };
|
||||||
}
|
}
|
||||||
|
|
||||||
function signalQualityText(dbm) {
|
function signalDbmText(dbm, raw = '') {
|
||||||
if (dbm >= -55) return '매우 강함';
|
if (dbm !== null && Number.isFinite(dbm)) {
|
||||||
if (dbm >= -67) return '강함';
|
return `${Number(dbm).toFixed(Number.isInteger(dbm) ? 0 : 1)} dBm`;
|
||||||
if (dbm >= -76) return '보통';
|
}
|
||||||
if (dbm >= -84) return '약함';
|
return String(raw || '').trim() || '-';
|
||||||
return '매우 약함';
|
}
|
||||||
|
|
||||||
|
function signalBadgeHtml(dbm, raw = '', key = '') {
|
||||||
|
const quality = signalQuality(dbm);
|
||||||
|
if (typeof quality === 'string') {
|
||||||
|
return `<span class="wifi-signal unknown">-</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyAttr = key ? ` data-signal-key="${attr(key)}"` : '';
|
||||||
|
return `<span class="wifi-signal ${quality.className}" tabindex="0" role="button" aria-describedby="signalTooltip" data-signal-dbm="${attr(signalDbmText(dbm, raw))}" data-signal-label="${attr(quality.label)}"${keyAttr}>${escapeHtml(quality.label)}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function editWifiHostname(mac, currentName) {
|
async function editWifiHostname(mac, currentName) {
|
||||||
@@ -1353,12 +1466,17 @@
|
|||||||
tableWrap.scrollLeft = scrollLeft;
|
tableWrap.scrollLeft = scrollLeft;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderWifiScan(data.wifi?.scan || null);
|
if (data.wifi && Object.prototype.hasOwnProperty.call(data.wifi, 'scan')) {
|
||||||
renderBluetoothScan(data.bluetooth?.scan || null);
|
renderWifiScan(data.wifi.scan || null);
|
||||||
|
}
|
||||||
|
if (data.bluetooth && Object.prototype.hasOwnProperty.call(data.bluetooth, 'scan')) {
|
||||||
|
renderBluetoothScan(data.bluetooth.scan || null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderWifiScan(scan) {
|
function renderWifiScan(scan) {
|
||||||
if (!els.wifiScan) return;
|
if (!els.wifiScan) return;
|
||||||
|
if (scanRenderHeld()) return;
|
||||||
const previousScroll = {};
|
const previousScroll = {};
|
||||||
els.wifiScan.querySelectorAll('.wifi-scan-band').forEach(section => {
|
els.wifiScan.querySelectorAll('.wifi-scan-band').forEach(section => {
|
||||||
const band = section.dataset.band || '';
|
const band = section.dataset.band || '';
|
||||||
@@ -1397,7 +1515,7 @@
|
|||||||
<td>${escapeHtml(row.ssid || '-')}</td>
|
<td>${escapeHtml(row.ssid || '-')}</td>
|
||||||
<td>${escapeHtml(row.bssid || '-')}</td>
|
<td>${escapeHtml(row.bssid || '-')}</td>
|
||||||
<td>${escapeHtml(row.channel ?? '-')} ${row.freq ? `(${escapeHtml(row.freq)}MHz)` : ''}</td>
|
<td>${escapeHtml(row.channel ?? '-')} ${row.freq ? `(${escapeHtml(row.freq)}MHz)` : ''}</td>
|
||||||
<td><span class="wifi-signal ${wifiSignalClass(dbm)}">${escapeHtml(row.signal || '-')}</span></td>
|
<td>${signalBadgeHtml(dbm, row.signal || '', `scan:${row.bssid || row.ssid || ''}`)}</td>
|
||||||
<td>${escapeHtml(row.security || '-')}</td>
|
<td>${escapeHtml(row.security || '-')}</td>
|
||||||
<td>${escapeHtml(wifiObservedText(row))}</td>
|
<td>${escapeHtml(wifiObservedText(row))}</td>
|
||||||
<td>${escapeHtml(wifiStandardText(row))}</td>
|
<td>${escapeHtml(wifiStandardText(row))}</td>
|
||||||
@@ -1432,6 +1550,7 @@
|
|||||||
|
|
||||||
function renderBluetoothScan(scan) {
|
function renderBluetoothScan(scan) {
|
||||||
if (!els.bluetoothScan) return;
|
if (!els.bluetoothScan) return;
|
||||||
|
if (scanRenderHeld()) return;
|
||||||
const wrap = els.bluetoothScan.querySelector('.table-wrap');
|
const wrap = els.bluetoothScan.querySelector('.table-wrap');
|
||||||
const scrollLeft = wrap ? wrap.scrollLeft : 0;
|
const scrollLeft = wrap ? wrap.scrollLeft : 0;
|
||||||
const devices = Array.isArray(scan?.devices) ? scan.devices : [];
|
const devices = Array.isArray(scan?.devices) ? scan.devices : [];
|
||||||
@@ -1450,7 +1569,7 @@
|
|||||||
<td>${escapeHtml(device.display_name || device.alias || device.name || '-')}</td>
|
<td>${escapeHtml(device.display_name || device.alias || device.name || '-')}</td>
|
||||||
<td>${escapeHtml(device.address || '-')}</td>
|
<td>${escapeHtml(device.address || '-')}</td>
|
||||||
<td>${escapeHtml(device.address_type || '-')}</td>
|
<td>${escapeHtml(device.address_type || '-')}</td>
|
||||||
<td><span class="wifi-signal ${bluetoothSignalClass(device.rssi)}">${escapeHtml(bluetoothRssiText(device.rssi))}</span></td>
|
<td>${signalBadgeHtml(bluetoothSignalDbm(device.rssi), bluetoothRssiText(device.rssi), `bt:${device.address || device.name || ''}`)}</td>
|
||||||
<td>${escapeHtml(device.tx_power ?? '-')}</td>
|
<td>${escapeHtml(device.tx_power ?? '-')}</td>
|
||||||
<td>${escapeHtml(booleanStateText(device.connected))}</td>
|
<td>${escapeHtml(booleanStateText(device.connected))}</td>
|
||||||
<td>${escapeHtml(booleanStateText(device.paired))}</td>
|
<td>${escapeHtml(booleanStateText(device.paired))}</td>
|
||||||
@@ -1488,14 +1607,9 @@
|
|||||||
return Number.isFinite(Number(value)) ? `${Number(value)} dBm` : '-';
|
return Number.isFinite(Number(value)) ? `${Number(value)} dBm` : '-';
|
||||||
}
|
}
|
||||||
|
|
||||||
function bluetoothSignalClass(value) {
|
function bluetoothSignalDbm(value) {
|
||||||
const dbm = Number(value);
|
const numeric = Number(value);
|
||||||
if (!Number.isFinite(dbm)) return 'unknown';
|
return Number.isFinite(numeric) ? numeric : null;
|
||||||
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) {
|
function booleanStateText(value) {
|
||||||
@@ -2124,6 +2238,7 @@
|
|||||||
renderSystemStatus(data);
|
renderSystemStatus(data);
|
||||||
renderWifi(data);
|
renderWifi(data);
|
||||||
renderCharts(data);
|
renderCharts(data);
|
||||||
|
updateSignalTooltip();
|
||||||
|
|
||||||
state.fanCauseTick = (state.fanCauseTick + 1) % 2;
|
state.fanCauseTick = (state.fanCauseTick + 1) % 2;
|
||||||
|
|
||||||
@@ -2359,6 +2474,56 @@
|
|||||||
els.statusBatteryRemaining?.addEventListener('blur', hideBatteryTooltip);
|
els.statusBatteryRemaining?.addEventListener('blur', hideBatteryTooltip);
|
||||||
window.addEventListener('resize', positionBatteryTooltip);
|
window.addEventListener('resize', positionBatteryTooltip);
|
||||||
window.addEventListener('scroll', positionBatteryTooltip, true);
|
window.addEventListener('scroll', positionBatteryTooltip, true);
|
||||||
|
document.addEventListener('mouseover', event => {
|
||||||
|
const target = event.target?.closest?.('.wifi-signal[data-signal-dbm]');
|
||||||
|
if (target) showSignalTooltip(target);
|
||||||
|
});
|
||||||
|
document.addEventListener('mouseout', event => {
|
||||||
|
const target = event.target?.closest?.('.wifi-signal[data-signal-dbm]');
|
||||||
|
if (!target) return;
|
||||||
|
const related = event.relatedTarget;
|
||||||
|
if (related && target.contains(related)) return;
|
||||||
|
hideSignalTooltip();
|
||||||
|
});
|
||||||
|
document.addEventListener('focusin', event => {
|
||||||
|
const target = event.target?.closest?.('.wifi-signal[data-signal-dbm]');
|
||||||
|
if (target) showSignalTooltip(target);
|
||||||
|
});
|
||||||
|
document.addEventListener('focusout', event => {
|
||||||
|
const target = event.target?.closest?.('.wifi-signal[data-signal-dbm]');
|
||||||
|
if (target) hideSignalTooltip();
|
||||||
|
});
|
||||||
|
document.addEventListener('pointerdown', event => {
|
||||||
|
const target = event.target?.closest?.('.wifi-signal[data-signal-dbm]');
|
||||||
|
if (target) {
|
||||||
|
showSignalTooltip(target);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hideSignalTooltip();
|
||||||
|
});
|
||||||
|
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 => {
|
[els.secondaryChartDetails, els.processDetails, els.diagnosticDetails].forEach(node => {
|
||||||
node?.addEventListener('toggle', resizeChartsSoon);
|
node?.addEventListener('toggle', resizeChartsSoon);
|
||||||
});
|
});
|
||||||
|
|||||||
+3
-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}
|
.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}
|
.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-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)}.nearby-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}.scan-empty{border:1px solid rgba(148,163,184,.35);background:rgba(148,163,184,.1);color:var(--text);border-radius:12px;padding:10px 12px;font-size:13px;line-height:1.5}
|
.wifi-signal{font-weight:700;font-variant-numeric:tabular-nums;cursor:help;text-decoration:underline;text-decoration-style:dotted;text-decoration-thickness:1px;text-underline-offset:3px}.wifi-signal.best{color:#16a34a}.wifi-signal.excellent{color:#22c55e}.wifi-signal.good{color:#38bdf8}.wifi-signal.fine{color:#0ea5e9}.wifi-signal.normal{color:#f59e0b}.wifi-signal.weak{color:#fb7185}.wifi-signal.bad{color:#ef4444}.wifi-signal.unknown{color:var(--sub);text-decoration:none;cursor:default}.nearby-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}.scan-empty{border:1px solid rgba(148,163,184,.35);background:rgba(148,163,184,.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}
|
.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}
|
.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}
|
.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}
|
||||||
@@ -355,6 +355,7 @@ html[data-theme="light"] .details-panel{background:#edf3fa;border-color:#b8c7da}
|
|||||||
</div>
|
</div>
|
||||||
<div id="notice" class="notice"></div>
|
<div id="notice" class="notice"></div>
|
||||||
<div id="batteryRuntimeTooltip" class="runtime-tooltip" role="tooltip" aria-hidden="true"></div>
|
<div id="batteryRuntimeTooltip" class="runtime-tooltip" role="tooltip" aria-hidden="true"></div>
|
||||||
|
<div id="signalTooltip" class="runtime-tooltip signal-tooltip" role="tooltip" aria-hidden="true"></div>
|
||||||
<div id="settingsDialog" class="dialog-layer settings-layer" aria-hidden="true">
|
<div id="settingsDialog" class="dialog-layer settings-layer" aria-hidden="true">
|
||||||
<div class="dialog-box settings-box" role="dialog" aria-modal="true" aria-labelledby="settingsDialogTitle">
|
<div class="dialog-box settings-box" role="dialog" aria-modal="true" aria-labelledby="settingsDialogTitle">
|
||||||
<div class="settings-head">
|
<div class="settings-head">
|
||||||
@@ -382,7 +383,7 @@ html[data-theme="light"] .details-panel{background:#edf3fa;border-color:#b8c7da}
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/assets/app.js?v=20260627_wifi_scan_readable1"></script>
|
<script src="/assets/app.js?v=20260627_scan_scroll_signal1"></script>
|
||||||
<script src="/assets/wakelock.js?v=20260607_prefs1"></script>
|
<script src="/assets/wakelock.js?v=20260607_prefs1"></script>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user