diff --git a/README.md b/README.md index 8381c0f..1b4fbf5 100644 --- a/README.md +++ b/README.md @@ -31,11 +31,11 @@ Car는 차량 모뎀/게이트웨이에서 받은 상태를 수집해 DB에 저 ## Monitor 경과 시간 -`monitor.php?mode=ajax`의 `meta.state_duration`은 현재 화면에 표시되는 차량 상태가 연속으로 유지된 시간입니다. 시동 값만 보지 않고 주행, 시동, 원격시동 준비/실행 상태를 함께 비교하며, 중간 수집 공백이 `RECEIVE_GAP_LIMIT_SEC`를 넘으면 같은 상태로 이어진 것으로 보지 않습니다. +`monitor.php?mode=ajax`의 `meta.state_duration`은 현재 시동 상태가 연속으로 유지된 시간입니다. 화면의 시동 카드 문장은 이 값을 사용해 시동 켜짐/꺼짐 경과를 표시합니다. -기존 시동 상태 기준 경과는 `meta.engine_state_duration`으로 분리했습니다. 시동이 꺼진 지 하루 이상 지난 상황에서도 화면 경과가 하루 단위로 튀지 않고, 실제 모니터링 데이터가 연속으로 유지된 시간만 보여주기 위한 구조입니다. +`meta.engine_state_duration`도 같은 기준의 값을 함께 내려주어 내부 확인과 호환성을 유지합니다. -로그 테이블의 경과 계산은 `YYYY-MM-DD HH:MM:SS` 값을 브라우저 기본 파서에 맡기지 않고 로컬 시간으로 직접 파싱합니다. 브라우저별 UTC/로컬 해석 차이로 로그 경과가 흔들리는 문제를 줄입니다. +상태 기록 섹션의 경과는 최신 로그 행을 기준으로 계산합니다. 최신 행은 0초이며, 아래 행은 최신 로그와의 시간 차이를 표시합니다. 각 행의 `age_seconds`는 서버에서 계산해 내려주므로 브라우저 시간차나 수집 주기에 의해 첫 행이 5초부터 시작하는 문제를 줄입니다. ## Monitor 화면 제어 diff --git a/monitor.php b/monitor.php index bcbea2d..0b4a1e0 100644 --- a/monitor.php +++ b/monitor.php @@ -534,6 +534,20 @@ if (isset($_GET['mode']) && $_GET['mode'] === 'ajax') { $stmtLogs->bindValue(':limit', LOG_LIMIT, PDO::PARAM_INT); $stmtLogs->execute(); $logs = $stmtLogs->fetchAll(); + $logReferenceTs = null; + if (!empty($logs[0]['ts'])) { + $parsedReferenceTs = strtotime((string)$logs[0]['ts']); + if ($parsedReferenceTs !== false) { + $logReferenceTs = $parsedReferenceTs; + } + } + foreach ($logs as &$logRow) { + $rowTs = strtotime((string)($logRow['ts'] ?? '')); + $logRow['age_seconds'] = ($logReferenceTs !== null && $rowTs !== false) + ? max(0, $logReferenceTs - $rowTs) + : 0; + } + unset($logRow); $stmtChart = $pdo->prepare("" . "SELECT ts, battery_voltage FROM (" @@ -572,7 +586,8 @@ if (isset($_GET['mode']) && $_GET['mode'] === 'ajax') { 'stale' => $isStale, 'stale_reason' => $staleReason, 'latest_tcp' => $latestTcp, - 'state_duration' => current_state_duration($pdo, $latest), + 'server_time' => time(), + 'state_duration' => current_engine_state_duration($pdo, $latest), 'engine_state_duration' => current_engine_state_duration($pdo, $latest), 'poll_interval' => $isFastPollState ? 5 : 10, 'log_count' => count($logs), @@ -1682,7 +1697,7 @@ if (isset($_GET['mode']) && $_GET['mode'] === 'usage') { .then(payload => { if (payload.status !== 'success') return; updateDashboard(payload.data, payload.meta || {}); - updateLogs(payload.logs || []); + updateLogs(payload.logs || [], payload.meta || {}); updateChart(payload.chart || []); usageRefreshIntervalSec = normalizedUsageInterval(payload.meta?.poll_interval); }) @@ -1872,14 +1887,17 @@ if (isset($_GET['mode']) && $_GET['mode'] === 'usage') { ).getTime(); } - function updateLogs(logs) { + function updateLogs(logs, meta = {}) { const tbody = document.getElementById('log-table-body'); + const serverNowMs = Number(meta.server_time || 0) > 0 ? Number(meta.server_time) * 1000 : Date.now(); const html = logs.map(log => { const cmdCode = String(log.cmd || '').toLowerCase(); const cmd = CMD_MAP[cmdCode] || { label: '', icon: 'fa-terminal', bg: 'rgba(128,128,128,0.1)', color: 'var(--text-secondary)' }; const cmdText = cmd.label ? t(cmd.label) : (cmdCode.toUpperCase() || '-'); const logMs = parseLocalTimestamp(log.ts); - const ageSec = logMs === null ? 0 : Math.max(0, Math.floor((Date.now() - logMs) / 1000)); + const ageSec = Number.isFinite(Number(log.age_seconds)) + ? Math.max(0, Number(log.age_seconds)) + : (logMs === null ? 0 : Math.max(0, Math.floor((serverNowMs - logMs) / 1000))); const doors = doorSummary(log); const remote = remoteSummary(log); const trim = escapeHtml(log.raw_trim || '-');