diff --git a/frontend/app.js b/frontend/app.js index 9eee482..cb6aad3 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -14,8 +14,9 @@ const svPrompt = document.getElementById('sv-prompt'); const svSpeed = document.getElementById('sv-speed'); const svTokens = document.getElementById('sv-tokens'); - const svLoadStage = document.getElementById('sv-load-stage'); - const svReqs = document.getElementById('sv-reqs'); + const svLoadStage = document.getElementById('sv-load-stage'); + const svSleepTimer = document.getElementById('sv-sleep-timer'); + const svReqs = document.getElementById('sv-reqs'); // ── State class management ──────────────────────────────────────────────── const ALL_STATES = [ @@ -47,7 +48,7 @@ } function update(msg) { - const { state, model, loading, metrics, request_count } = msg; + const { state, model, loading, metrics, request_count, sleeping_since } = msg; applyState(state); @@ -92,6 +93,13 @@ svPrompt.textContent = svSpeed.textContent = svTokens.textContent = '—'; } + // Sleep timer + if (state === 'sleeping' && sleeping_since) { + startSleepTimer(sleeping_since); + } else { + stopSleepTimer(); + } + // Pulse dot on message wsDot.classList.remove('pulse'); void wsDot.offsetWidth; // force reflow to restart animation @@ -130,6 +138,40 @@ document.addEventListener('touchmove', e => { if (dragging) { e.preventDefault(); onDrag(e.touches[0].clientX); } }, { passive: false }); document.addEventListener('touchend', endDrag); + // ── Sleep timer ─────────────────────────────────────────────────────────── + let sleepingSince = null; + let sleepInterval = null; + + function formatDuration(sec) { + const h = Math.floor(sec / 3600); + const m = Math.floor((sec % 3600) / 60); + const s = sec % 60; + if (h > 0) return h + 'h ' + m + 'm'; + if (m > 0) return m + 'm ' + s + 's'; + return s + 's'; + } + + function tickSleepTimer() { + if (!sleepingSince) return; + const elapsed = Math.floor((Date.now() - new Date(sleepingSince).getTime()) / 1000); + svSleepTimer.textContent = formatDuration(elapsed); + } + + function startSleepTimer(since) { + if (sleepingSince === since) return; + sleepingSince = since; + clearInterval(sleepInterval); + tickSleepTimer(); + sleepInterval = setInterval(tickSleepTimer, 1000); + } + + function stopSleepTimer() { + sleepingSince = null; + clearInterval(sleepInterval); + sleepInterval = null; + svSleepTimer.textContent = '—'; + } + // ── HTTP polling fallback ───────────────────────────────────────────────── let pollTimer = null; const POLL_MS = 2000; diff --git a/frontend/index.html b/frontend/index.html index 3c179fb..8902875 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -147,6 +147,10 @@ STAGE +
+ ASLEEP + +
REQS 0 diff --git a/frontend/style.css b/frontend/style.css index 22e990e..9b79c61 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -340,6 +340,8 @@ body.state-sleeping #row-speed, body.state-sleeping #row-tokens, body.state-sleeping #row-load-stage { display: none; } +body:not(.state-sleeping) #row-sleep-timer { display: none; } + body.state-waiting #row-prompt, body.state-waiting #row-speed, body.state-waiting #row-tokens, diff --git a/log_parser.py b/log_parser.py index 99cc075..b077a4a 100644 --- a/log_parser.py +++ b/log_parser.py @@ -51,6 +51,7 @@ class ServerState: loading: Optional[LoadingInfo] = None metrics: Metrics = field(default_factory=Metrics) request_count: int = 0 + sleeping_since: Optional[str] = None # ISO timestamp when sleeping started def to_dict(self) -> dict: return { @@ -59,6 +60,7 @@ class ServerState: "loading": asdict(self.loading) if self.loading else None, "metrics": asdict(self.metrics), "request_count": self.request_count, + "sleeping_since": self.sleeping_since, "timestamp": datetime.now().isoformat(timespec="seconds"), } @@ -215,12 +217,17 @@ class LogParser: self._state.model = model_id return self._set(state="idle", loading=None) if s == "sleeping": - return self._set(state="sleeping") + return self._set( + state="sleeping", + sleeping_since=datetime.now().isoformat(timespec="seconds"), + ) return None # ── Helpers ────────────────────────────────────────────────────────────── def _set(self, **kwargs) -> ServerState: + if "state" in kwargs and kwargs["state"] != "sleeping": + self._state.sleeping_since = None for k, v in kwargs.items(): setattr(self._state, k, v) return self._state