Add monitor wake lock toggle
This commit is contained in:
@@ -14,6 +14,7 @@ Car는 차량 모뎀/게이트웨이에서 받은 상태를 수집해 DB에 저
|
|||||||
- 차량 상태 API
|
- 차량 상태 API
|
||||||
- 허용된 차량 명령 TCP 전송
|
- 허용된 차량 명령 TCP 전송
|
||||||
- monitor 화면과 AJAX 상태 갱신
|
- monitor 화면과 AJAX 상태 갱신
|
||||||
|
- monitor 화면 WakeLock, 언어, 테마 토글
|
||||||
- 사용량/요금 표시와 보정 metadata
|
- 사용량/요금 표시와 보정 metadata
|
||||||
- 4.95초 UI timeout과 60초 사용량 정산 분리
|
- 4.95초 UI timeout과 60초 사용량 정산 분리
|
||||||
- TCP 실패 reason과 마지막 수신 지연 표시
|
- TCP 실패 reason과 마지막 수신 지연 표시
|
||||||
@@ -36,6 +37,10 @@ Car는 차량 모뎀/게이트웨이에서 받은 상태를 수집해 DB에 저
|
|||||||
|
|
||||||
로그 테이블의 경과 계산은 `YYYY-MM-DD HH:MM:SS` 값을 브라우저 기본 파서에 맡기지 않고 로컬 시간으로 직접 파싱합니다. 브라우저별 UTC/로컬 해석 차이로 로그 경과가 흔들리는 문제를 줄입니다.
|
로그 테이블의 경과 계산은 `YYYY-MM-DD HH:MM:SS` 값을 브라우저 기본 파서에 맡기지 않고 로컬 시간으로 직접 파싱합니다. 브라우저별 UTC/로컬 해석 차이로 로그 경과가 흔들리는 문제를 줄입니다.
|
||||||
|
|
||||||
|
## Monitor 화면 제어
|
||||||
|
|
||||||
|
헤더 우측 컨트롤은 WakeLock, 언어, 테마 순서입니다. WakeLock은 Screen Wake Lock API를 지원하는 브라우저에서 화면 꺼짐을 방지하며, 활성 상태는 초록색 아이콘 버튼으로 표시합니다. 사용자의 선택은 `localStorage`에 저장하고, 새로고침이나 탭 복귀 후에는 브라우저 정책에 맞춰 다시 획득을 시도합니다.
|
||||||
|
|
||||||
## 구성
|
## 구성
|
||||||
|
|
||||||
- `api.php`: 차량 상태/제어 API
|
- `api.php`: 차량 상태/제어 API
|
||||||
|
|||||||
+93
-10
@@ -875,20 +875,18 @@ if (isset($_GET['mode']) && $_GET['mode'] === 'usage') {
|
|||||||
font-family: 'SF Mono', monospace; font-size: 0.75rem;
|
font-family: 'SF Mono', monospace; font-size: 0.75rem;
|
||||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 180px; margin: 0 auto;
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 180px; margin: 0 auto;
|
||||||
}
|
}
|
||||||
.btn-theme-toggle {
|
.btn-icon-toggle {
|
||||||
width: 40px; height: 40px; border-radius: 50%; border: 1px solid var(--card-border);
|
width: 40px; height: 40px; border-radius: 50%; border: 1px solid var(--card-border);
|
||||||
background: var(--card-bg); color: var(--text-primary);
|
background: var(--card-bg); color: var(--text-primary);
|
||||||
display: flex; align-items: center; justify-content: center; cursor: pointer;
|
display: flex; align-items: center; justify-content: center; cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
.btn-theme-toggle:hover { background: var(--bg-body); transform: scale(1.05); }
|
.btn-icon-toggle:hover { background: var(--bg-body); transform: scale(1.05); }
|
||||||
.btn-lang-toggle {
|
.btn-icon-toggle.active {
|
||||||
width: 40px; height: 40px; border-radius: 50%; border: 1px solid var(--card-border);
|
background: var(--box-bg-green);
|
||||||
background: var(--card-bg); color: var(--text-primary);
|
border-color: rgba(34, 197, 94, 0.45);
|
||||||
display: flex; align-items: center; justify-content: center; cursor: pointer;
|
color: var(--accent-green);
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
}
|
||||||
.btn-lang-toggle:hover { background: var(--bg-body); transform: scale(1.05); }
|
|
||||||
|
|
||||||
@keyframes pulse-ring {
|
@keyframes pulse-ring {
|
||||||
0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); }
|
0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); }
|
||||||
@@ -1041,10 +1039,13 @@ if (isset($_GET['mode']) && $_GET['mode'] === 'usage') {
|
|||||||
<div class="label-text mb-0" data-i18n="lastData">LAST DATA</div>
|
<div class="label-text mb-0" data-i18n="lastData">LAST DATA</div>
|
||||||
<div id="last-updated" class="font-mono fw-bold text-primary">--:--:--</div>
|
<div id="last-updated" class="font-mono fw-bold text-primary">--:--:--</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-lang-toggle shadow-sm" id="lang-btn" onclick="toggleLanguage()" title="Language">
|
<button class="btn-icon-toggle shadow-sm" id="wakelock-btn" onclick="toggleWakeLock()" title="WakeLock">
|
||||||
|
<i class="fas fa-mobile-screen-button"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn-icon-toggle shadow-sm" id="lang-btn" onclick="toggleLanguage()" title="Language">
|
||||||
<i class="fas fa-globe"></i>
|
<i class="fas fa-globe"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-theme-toggle shadow-sm" id="theme-btn" onclick="toggleTheme()">
|
<button class="btn-icon-toggle shadow-sm" id="theme-btn" onclick="toggleTheme()">
|
||||||
<i class="fas fa-moon"></i>
|
<i class="fas fa-moon"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1371,6 +1372,8 @@ if (isset($_GET['mode']) && $_GET['mode'] === 'usage') {
|
|||||||
engineStateOnSentence: '시동이 {duration} 켜져있음',
|
engineStateOnSentence: '시동이 {duration} 켜져있음',
|
||||||
engineStateOffSentence: '시동이 {duration} 꺼져있음',
|
engineStateOffSentence: '시동이 {duration} 꺼져있음',
|
||||||
dataUsageTitle: '{month} 데이터 사용량 / {days}일 남음',
|
dataUsageTitle: '{month} 데이터 사용량 / {days}일 남음',
|
||||||
|
wakeLockTitle: '화면 켜짐 유지',
|
||||||
|
wakeLockUnsupported: '화면 켜짐 유지 미지원',
|
||||||
languageTitle: '언어 변경',
|
languageTitle: '언어 변경',
|
||||||
rawView: '원본 보기',
|
rawView: '원본 보기',
|
||||||
cmdStatus: '상태',
|
cmdStatus: '상태',
|
||||||
@@ -1434,6 +1437,8 @@ if (isset($_GET['mode']) && $_GET['mode'] === 'usage') {
|
|||||||
engineStateOnSentence: 'Engine has been ON for {duration}',
|
engineStateOnSentence: 'Engine has been ON for {duration}',
|
||||||
engineStateOffSentence: 'Engine has been OFF for {duration}',
|
engineStateOffSentence: 'Engine has been OFF for {duration}',
|
||||||
dataUsageTitle: '{month} Data Usage / {days}d left',
|
dataUsageTitle: '{month} Data Usage / {days}d left',
|
||||||
|
wakeLockTitle: 'Keep screen awake',
|
||||||
|
wakeLockUnsupported: 'WakeLock unavailable',
|
||||||
languageTitle: 'Change language',
|
languageTitle: 'Change language',
|
||||||
rawView: 'View RAW',
|
rawView: 'View RAW',
|
||||||
cmdStatus: 'STATUS',
|
cmdStatus: 'STATUS',
|
||||||
@@ -1470,16 +1475,25 @@ if (isset($_GET['mode']) && $_GET['mode'] === 'usage') {
|
|||||||
let usageTimer = null;
|
let usageTimer = null;
|
||||||
let usageRefreshIntervalSec = DEFAULT_USAGE_REFRESH_INTERVAL_SEC;
|
let usageRefreshIntervalSec = DEFAULT_USAGE_REFRESH_INTERVAL_SEC;
|
||||||
let usageInFlight = false;
|
let usageInFlight = false;
|
||||||
|
let wakeLockSentinel = null;
|
||||||
|
let wakeLockWanted = false;
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
initLanguage();
|
initLanguage();
|
||||||
initTheme();
|
initTheme();
|
||||||
|
initWakeLock();
|
||||||
initChart();
|
initChart();
|
||||||
fetchData();
|
fetchData();
|
||||||
refreshTimer = setInterval(fetchData, 1000);
|
refreshTimer = setInterval(fetchData, 1000);
|
||||||
fetchUsage();
|
fetchUsage();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
if (document.visibilityState === 'visible' && wakeLockWanted) {
|
||||||
|
requestWakeLock();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function initLanguage() {
|
function initLanguage() {
|
||||||
const saved = localStorage.getItem('lang');
|
const saved = localStorage.getItem('lang');
|
||||||
const browser = (navigator.language || navigator.userLanguage || 'ko').toLowerCase();
|
const browser = (navigator.language || navigator.userLanguage || 'ko').toLowerCase();
|
||||||
@@ -1515,6 +1529,7 @@ if (isset($_GET['mode']) && $_GET['mode'] === 'usage') {
|
|||||||
langBtn.title = t('languageTitle');
|
langBtn.title = t('languageTitle');
|
||||||
langBtn.setAttribute('aria-label', t('languageTitle'));
|
langBtn.setAttribute('aria-label', t('languageTitle'));
|
||||||
}
|
}
|
||||||
|
updateWakeLockButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
function initTheme() {
|
function initTheme() {
|
||||||
@@ -1539,6 +1554,74 @@ if (isset($_GET['mode']) && $_GET['mode'] === 'usage') {
|
|||||||
icon.className = theme === 'dark' ? 'fas fa-sun text-warning' : 'fas fa-moon text-secondary';
|
icon.className = theme === 'dark' ? 'fas fa-sun text-warning' : 'fas fa-moon text-secondary';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initWakeLock() {
|
||||||
|
wakeLockWanted = localStorage.getItem('wakeLock') === 'on';
|
||||||
|
updateWakeLockButton();
|
||||||
|
if (wakeLockWanted) {
|
||||||
|
requestWakeLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function wakeLockSupported() {
|
||||||
|
return 'wakeLock' in navigator;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestWakeLock() {
|
||||||
|
if (!wakeLockSupported()) {
|
||||||
|
wakeLockWanted = false;
|
||||||
|
localStorage.setItem('wakeLock', 'off');
|
||||||
|
updateWakeLockButton();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
wakeLockSentinel = await navigator.wakeLock.request('screen');
|
||||||
|
wakeLockSentinel.addEventListener('release', () => {
|
||||||
|
wakeLockSentinel = null;
|
||||||
|
updateWakeLockButton();
|
||||||
|
});
|
||||||
|
updateWakeLockButton();
|
||||||
|
} catch (error) {
|
||||||
|
wakeLockSentinel = null;
|
||||||
|
updateWakeLockButton();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function releaseWakeLock() {
|
||||||
|
if (wakeLockSentinel) {
|
||||||
|
try {
|
||||||
|
await wakeLockSentinel.release();
|
||||||
|
} catch (error) {
|
||||||
|
// Browser may already have released it on visibility or power policy changes.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wakeLockSentinel = null;
|
||||||
|
updateWakeLockButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleWakeLock() {
|
||||||
|
wakeLockWanted = !wakeLockWanted;
|
||||||
|
localStorage.setItem('wakeLock', wakeLockWanted ? 'on' : 'off');
|
||||||
|
if (wakeLockWanted) {
|
||||||
|
requestWakeLock();
|
||||||
|
} else {
|
||||||
|
releaseWakeLock();
|
||||||
|
}
|
||||||
|
updateWakeLockButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateWakeLockButton() {
|
||||||
|
const btn = document.getElementById('wakelock-btn');
|
||||||
|
if (!btn) return;
|
||||||
|
|
||||||
|
const supported = wakeLockSupported();
|
||||||
|
const active = Boolean(wakeLockSentinel);
|
||||||
|
btn.classList.toggle('active', active);
|
||||||
|
btn.disabled = !supported;
|
||||||
|
btn.title = supported ? t('wakeLockTitle') : t('wakeLockUnsupported');
|
||||||
|
btn.setAttribute('aria-label', btn.title);
|
||||||
|
}
|
||||||
|
|
||||||
function getChartColors() {
|
function getChartColors() {
|
||||||
const style = getComputedStyle(document.body);
|
const style = getComputedStyle(document.body);
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user