Control WiFi 진단과 배터리 예측 보강
This commit is contained in:
+132
-6
@@ -15,6 +15,7 @@
|
||||
wifi24: $('#wifi24'),
|
||||
wifi5: $('#wifi5'),
|
||||
wifiTable: $('#wifiTable'),
|
||||
wifiScan: $('#wifiScan'),
|
||||
statusHost: $('#statusHost'),
|
||||
statusLoad: $('#statusLoad'),
|
||||
statusUsers: $('#statusUsers'),
|
||||
@@ -145,6 +146,15 @@
|
||||
remaining: 'Remaining',
|
||||
batteryVoltage: 'Battery Voltage',
|
||||
wifiClients: 'WiFi Clients',
|
||||
wifiScan: 'Nearby WiFi Scan',
|
||||
wifiScanDisabled: 'Nearby WiFi scan is disabled.',
|
||||
wifiScanEmpty: 'No nearby WiFi scan results.',
|
||||
wifiScanCached: 'cached',
|
||||
wifiScanFresh: 'fresh',
|
||||
ssid: 'SSID',
|
||||
channel: 'Channel',
|
||||
security: 'Security',
|
||||
lastSeen: 'Last Seen',
|
||||
band: 'Band',
|
||||
hostname: 'Hostname',
|
||||
signal: 'Signal',
|
||||
@@ -286,6 +296,15 @@
|
||||
remaining: '잔여 시간',
|
||||
batteryVoltage: '배터리 전압',
|
||||
wifiClients: 'WiFi 클라이언트',
|
||||
wifiScan: '주변 WiFi 스캔',
|
||||
wifiScanDisabled: '주변 WiFi 스캔이 꺼져 있습니다.',
|
||||
wifiScanEmpty: '주변 WiFi 스캔 결과가 없습니다.',
|
||||
wifiScanCached: '캐시',
|
||||
wifiScanFresh: '실시간',
|
||||
ssid: 'SSID',
|
||||
channel: '채널',
|
||||
security: '보안',
|
||||
lastSeen: '마지막 감지',
|
||||
band: '대역',
|
||||
hostname: '호스트명',
|
||||
signal: '신호',
|
||||
@@ -909,6 +928,14 @@
|
||||
if (remaining.seconds !== undefined && remaining.seconds !== null) {
|
||||
lines.push(`계산 잔여: ${formatDurationSeconds(remaining.seconds)}`);
|
||||
}
|
||||
if (remaining.runtime_cap_seconds !== undefined && remaining.runtime_cap_seconds !== null) {
|
||||
const capText = formatDurationSeconds(remaining.runtime_cap_seconds);
|
||||
const cappedText = remaining.runtime_capped ? ' / 현실 상한 적용' : '';
|
||||
lines.push(`현실 상한: ${capText}${cappedText}`);
|
||||
}
|
||||
if (remaining.original_seconds !== undefined && remaining.original_seconds !== null) {
|
||||
lines.push(`상한 전 계산값: ${formatDurationSeconds(remaining.original_seconds)}`);
|
||||
}
|
||||
if (remaining.drop_per_hour !== undefined && remaining.drop_per_hour !== null) {
|
||||
lines.push(`시간당 SOC 감소: ${Number(remaining.drop_per_hour).toFixed(3)}%`);
|
||||
}
|
||||
@@ -960,28 +987,33 @@
|
||||
lines.push('세부 후보');
|
||||
remaining.windows.forEach(row => {
|
||||
if (row.source === 'learned_profile') {
|
||||
lines.push(`- 장기 학습: ${row.intervals || 0}구간 / SOC대 ${row.soc_buckets || 0}개 / 누적감소 ${Number(row.drop_percent || 0).toFixed(3)}% / 신뢰도 ${(Number(row.confidence || 0) * 100).toFixed(0)}%`);
|
||||
const cap = row.runtime_capped ? ` / 상한 전 ${formatDurationSeconds(row.original_seconds)}` : '';
|
||||
lines.push(`- 장기 학습: ${row.intervals || 0}구간 / SOC대 ${row.soc_buckets || 0}개 / 누적감소 ${Number(row.drop_percent || 0).toFixed(3)}% / 신뢰도 ${(Number(row.confidence || 0) * 100).toFixed(0)}%${cap}`);
|
||||
} else if (row.source === 'energy_profile') {
|
||||
const wh = row.wh_per_percent === null || row.wh_per_percent === undefined ? '-' : `${Number(row.wh_per_percent).toFixed(4)}Wh/%`;
|
||||
const stability = row.stability === null || row.stability === undefined ? '' : ` / 안정도 ${(Number(row.stability) * 100).toFixed(0)}%`;
|
||||
lines.push(`- 에너지 학습: ${row.intervals || 0}구간 / SOC대 ${row.soc_buckets || 0}개 / ${wh} / 신뢰도 ${(Number(row.confidence || 0) * 100).toFixed(0)}%${stability}`);
|
||||
const cap = row.runtime_capped ? ` / 상한 전 ${formatDurationSeconds(row.original_seconds)}` : '';
|
||||
lines.push(`- 에너지 학습: ${row.intervals || 0}구간 / SOC대 ${row.soc_buckets || 0}개 / ${wh} / 신뢰도 ${(Number(row.confidence || 0) * 100).toFixed(0)}%${stability}${cap}`);
|
||||
} else if (row.source === 'capacity_power_model') {
|
||||
const capacity = row.capacity_wh === null || row.capacity_wh === undefined ? '-' : `${Number(row.capacity_wh).toFixed(2)}Wh`;
|
||||
const stability = row.stability === null || row.stability === undefined ? '' : ` / 안정도 ${(Number(row.stability) * 100).toFixed(0)}%`;
|
||||
lines.push(`- 용량 전력 모델: ${capacity} / 신뢰도 ${(Number(row.confidence || 0) * 100).toFixed(0)}%${stability}`);
|
||||
const cap = row.runtime_capped ? ` / 상한 전 ${formatDurationSeconds(row.original_seconds)}` : '';
|
||||
lines.push(`- 용량 전력 모델: ${capacity} / 신뢰도 ${(Number(row.confidence || 0) * 100).toFixed(0)}%${stability}${cap}`);
|
||||
} else if (row.source === 'voltage_slope') {
|
||||
const load = row.load_factor === null || row.load_factor === undefined ? '-' : `${Number(row.load_factor).toFixed(3)}x`;
|
||||
const kept = row.kept_ratio === null || row.kept_ratio === undefined ? '' : ` / 유효점 ${(Number(row.kept_ratio) * 100).toFixed(0)}%`;
|
||||
const stability = row.stability === null || row.stability === undefined ? '' : ` / 안정도 ${(Number(row.stability) * 100).toFixed(0)}%`;
|
||||
const floor = row.voltage_floor === null || row.voltage_floor === undefined ? '' : ` / 하한 ${Number(row.voltage_floor).toFixed(3)}V`;
|
||||
const volts = row.volts_per_hour === null || row.volts_per_hour === undefined ? '' : ` / ${Number(row.volts_per_hour).toFixed(4)}V/h`;
|
||||
lines.push(`- 전압 ${row.minutes}분: 신뢰도 ${(Number(row.confidence || 0) * 100).toFixed(0)}% / 부하보정 ${load}${kept}${stability}${floor}${volts}`);
|
||||
const cap = row.runtime_capped ? ` / 상한 전 ${formatDurationSeconds(row.original_seconds)}` : '';
|
||||
lines.push(`- 전압 ${row.minutes}분: 신뢰도 ${(Number(row.confidence || 0) * 100).toFixed(0)}% / 부하보정 ${load}${kept}${stability}${floor}${volts}${cap}`);
|
||||
} else {
|
||||
const load = row.load_factor === null || row.load_factor === undefined ? '-' : `${Number(row.load_factor).toFixed(3)}x`;
|
||||
const prefix = row.source === 'robust_regression' ? '강건 회귀' : '최근 추세';
|
||||
const kept = row.kept_ratio === null || row.kept_ratio === undefined ? '' : ` / 유효점 ${(Number(row.kept_ratio) * 100).toFixed(0)}%`;
|
||||
const stability = row.stability === null || row.stability === undefined ? '' : ` / 안정도 ${(Number(row.stability) * 100).toFixed(0)}%`;
|
||||
lines.push(`- ${prefix} ${row.minutes}분: 감소 ${Number(row.drop_percent || 0).toFixed(3)}% / 신뢰도 ${(Number(row.confidence || 0) * 100).toFixed(0)}% / 부하보정 ${load}${kept}${stability}`);
|
||||
const cap = row.runtime_capped ? ` / 상한 전 ${formatDurationSeconds(row.original_seconds)}` : '';
|
||||
lines.push(`- ${prefix} ${row.minutes}분: 감소 ${Number(row.drop_percent || 0).toFixed(3)}% / 신뢰도 ${(Number(row.confidence || 0) * 100).toFixed(0)}% / 부하보정 ${load}${kept}${stability}${cap}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1091,7 +1123,36 @@
|
||||
}
|
||||
|
||||
function wifiMetricCell(row, key) {
|
||||
return `<td>${escapeHtml(row[key] || '-')}</td>`;
|
||||
if (key !== 'signal') {
|
||||
return `<td>${escapeHtml(row[key] || '-')}</td>`;
|
||||
}
|
||||
|
||||
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>`;
|
||||
}
|
||||
|
||||
function wifiSignalDbm(value) {
|
||||
const match = String(value ?? '').match(/-?\d+(?:\.\d+)?/);
|
||||
return match ? Number(match[0]) : null;
|
||||
}
|
||||
|
||||
function wifiSignalClass(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';
|
||||
}
|
||||
|
||||
function signalQualityText(dbm) {
|
||||
if (dbm >= -55) return '매우 강함';
|
||||
if (dbm >= -67) return '강함';
|
||||
if (dbm >= -76) return '보통';
|
||||
if (dbm >= -84) return '약함';
|
||||
return '매우 약함';
|
||||
}
|
||||
|
||||
async function editWifiHostname(mac, currentName) {
|
||||
@@ -1255,6 +1316,71 @@
|
||||
</tr>
|
||||
`).join('')
|
||||
: `<tr><td colspan="9">${escapeHtml(t('noWifiClients'))}</td></tr>`;
|
||||
|
||||
renderWifiScan(data.wifi?.scan || null);
|
||||
}
|
||||
|
||||
function renderWifiScan(scan) {
|
||||
if (!els.wifiScan) return;
|
||||
if (!scan || scan.enabled === false) {
|
||||
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 groups = ['2.4G', '5G'].map(band => {
|
||||
const bandRows = networks.filter(row => row.band === band);
|
||||
return `
|
||||
<section class="wifi-scan-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>
|
||||
<tbody>
|
||||
${bandRows.length ? bandRows.map(row => {
|
||||
const dbm = wifiSignalDbm(row.signal);
|
||||
return `<tr>
|
||||
<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>${escapeHtml(row.security || '-')}</td>
|
||||
<td>${row.last_seen_ms === null || row.last_seen_ms === undefined ? '-' : `${escapeHtml(row.last_seen_ms)}ms`}</td>
|
||||
<td>${escapeHtml(wifiStandardText(row))}</td>
|
||||
<td>${escapeHtml(row.channel_width || '-')}</td>
|
||||
<td>${escapeHtml(row.rates || '-')}</td>
|
||||
<td>${escapeHtml(row.capability || '-')}</td>
|
||||
</tr>`;
|
||||
}).join('') : `<tr><td colspan="10">${escapeHtml(t('wifiScanEmpty'))}</td></tr>`}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
}).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>
|
||||
`;
|
||||
}
|
||||
|
||||
function wifiStandardText(row) {
|
||||
const parts = [];
|
||||
if (row?.he) parts.push('WiFi 6');
|
||||
if (row?.vht) parts.push('WiFi 5');
|
||||
if (row?.ht) parts.push('WiFi 4');
|
||||
return parts.join(' / ') || '-';
|
||||
}
|
||||
|
||||
function renderSystemStatus(data) {
|
||||
|
||||
Reference in New Issue
Block a user