스캔 표 스크롤과 신호 표시를 다듬음

This commit is contained in:
seo
2026-06-27 02:50:11 +09:00
parent 6734801b52
commit 9936ae6985
3 changed files with 199 additions and 33 deletions
+192 -27
View File
@@ -17,6 +17,7 @@
wifiTable: $('#wifiTable'),
wifiScan: $('#wifiScan'),
bluetoothScan: $('#bluetoothScan'),
signalTooltip: $('#signalTooltip'),
statusHost: $('#statusHost'),
statusLoad: $('#statusLoad'),
statusUsers: $('#statusUsers'),
@@ -86,6 +87,11 @@
theme: 'dark',
batteryTooltipOpen: false,
batteryTooltipText: '',
signalTooltipOpen: false,
signalTooltipTarget: null,
signalTooltipKey: '',
signalTooltipText: '',
scanScrollHoldUntil: 0,
settingsPayload: null,
settingsOpen: false,
settingsDirty: false,
@@ -1108,6 +1114,104 @@
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) {
setText(els.updated, data.generated_at || '-');
setText(els.temp, Number(data.system?.temp_c || 0).toFixed(1) + '°C');
@@ -1159,9 +1263,7 @@
}
const dbm = wifiSignalDbm(row[key]);
const cls = wifiSignalClass(dbm);
const title = dbm === null ? '' : `title="${attr(signalQualityText(dbm))}"`;
return `<td><span class="wifi-signal ${cls}" ${title}>${escapeHtml(row[key] || '-')}</span></td>`;
return `<td>${signalBadgeHtml(dbm, row[key] || '', `client:${row.mac || row.ip || row.hostname || ''}`)}</td>`;
}
function wifiSignalDbm(value) {
@@ -1169,21 +1271,32 @@
return match ? Number(match[0]) : null;
}
function wifiSignalClass(dbm) {
function signalQuality(dbm) {
if (dbm === null || !Number.isFinite(dbm)) return 'unknown';
if (dbm >= -55) return 'excellent';
if (dbm >= -67) return 'good';
if (dbm >= -76) return 'fair';
if (dbm >= -84) return 'weak';
return 'bad';
if (dbm >= -45) return { className: 'best', label: '최상' };
if (dbm >= -55) return { className: 'excellent', label: '매우 좋음' };
if (dbm >= -65) return { className: 'good', label: '좋음' };
if (dbm >= -72) return { className: 'fine', label: '양호' };
if (dbm >= -80) return { className: 'normal', label: '보통' };
if (dbm >= -88) return { className: 'weak', label: '약함' };
return { className: 'bad', label: '매우 약함' };
}
function signalQualityText(dbm) {
if (dbm >= -55) return '매우 강함';
if (dbm >= -67) return '강함';
if (dbm >= -76) return '보통';
if (dbm >= -84) return '약함';
return '매우 약함';
function signalDbmText(dbm, raw = '') {
if (dbm !== null && Number.isFinite(dbm)) {
return `${Number(dbm).toFixed(Number.isInteger(dbm) ? 0 : 1)} dBm`;
}
return String(raw || '').trim() || '-';
}
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) {
@@ -1353,12 +1466,17 @@
tableWrap.scrollLeft = scrollLeft;
}
renderWifiScan(data.wifi?.scan || null);
renderBluetoothScan(data.bluetooth?.scan || null);
if (data.wifi && Object.prototype.hasOwnProperty.call(data.wifi, 'scan')) {
renderWifiScan(data.wifi.scan || null);
}
if (data.bluetooth && Object.prototype.hasOwnProperty.call(data.bluetooth, 'scan')) {
renderBluetoothScan(data.bluetooth.scan || null);
}
}
function renderWifiScan(scan) {
if (!els.wifiScan) return;
if (scanRenderHeld()) return;
const previousScroll = {};
els.wifiScan.querySelectorAll('.wifi-scan-band').forEach(section => {
const band = section.dataset.band || '';
@@ -1397,7 +1515,7 @@
<td>${escapeHtml(row.ssid || '-')}</td>
<td>${escapeHtml(row.bssid || '-')}</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(wifiObservedText(row))}</td>
<td>${escapeHtml(wifiStandardText(row))}</td>
@@ -1432,6 +1550,7 @@
function renderBluetoothScan(scan) {
if (!els.bluetoothScan) return;
if (scanRenderHeld()) return;
const wrap = els.bluetoothScan.querySelector('.table-wrap');
const scrollLeft = wrap ? wrap.scrollLeft : 0;
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.address || '-')}</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(booleanStateText(device.connected))}</td>
<td>${escapeHtml(booleanStateText(device.paired))}</td>
@@ -1488,14 +1607,9 @@
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 bluetoothSignalDbm(value) {
const numeric = Number(value);
return Number.isFinite(numeric) ? numeric : null;
}
function booleanStateText(value) {
@@ -2124,6 +2238,7 @@
renderSystemStatus(data);
renderWifi(data);
renderCharts(data);
updateSignalTooltip();
state.fanCauseTick = (state.fanCauseTick + 1) % 2;
@@ -2359,6 +2474,56 @@
els.statusBatteryRemaining?.addEventListener('blur', hideBatteryTooltip);
window.addEventListener('resize', positionBatteryTooltip);
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 => {
node?.addEventListener('toggle', resizeChartsSoon);
});
+3 -2
View File
@@ -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)}.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}
.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}
@@ -355,6 +355,7 @@ html[data-theme="light"] .details-panel{background:#edf3fa;border-color:#b8c7da}
</div>
<div id="notice" class="notice"></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 class="dialog-box settings-box" role="dialog" aria-modal="true" aria-labelledby="settingsDialogTitle">
<div class="settings-head">
@@ -382,7 +383,7 @@ html[data-theme="light"] .details-panel{background:#edf3fa;border-color:#b8c7da}
</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>
<?php endif; ?>
</body>