linux observer
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Fix tray uptime and menu update churn

Add a dedicated _start_mono timestamp that is initialized once when the observer starts and use it for uptime reporting in the tray and D-Bus stats service instead of start_at_mono, which resets on each segment boundary.

Also guard every update_item() call in TrayApp._update_live_stats() behind label-change checks so unchanged live stats do not emit redundant D-Bus LayoutUpdated signals and cause the tray menu to blink every 5 seconds.

Update the tray and D-Bus tests to seed _start_mono on mock observers and add a regression test covering the no-op live stats update path.

+43 -12
+1 -1
src/solstone_linux/dbus_service.py
··· 127 127 synced_days = len(self._observer._sync._synced_days) 128 128 129 129 total_size_mb = int(total_size / (1024 * 1024)) 130 - uptime_seconds = int(time.monotonic() - self._observer.start_at_mono) 130 + uptime_seconds = int(time.monotonic() - self._observer._start_mono) 131 131 132 132 return { 133 133 "captures_today": Variant("i", captures_today),
+1
src/solstone_linux/observer.py
··· 91 91 # State tracking 92 92 self.start_at = time.time() 93 93 self.start_at_mono = time.monotonic() 94 + self._start_mono = time.monotonic() 94 95 self.threshold_hits = 0 95 96 self.accumulated_audio_buffer = np.array([], dtype=np.float32).reshape(0, 2) 96 97
+21 -11
src/solstone_linux/tray.py
··· 190 190 synced_days = len(obs._sync._synced_days) 191 191 192 192 total_size_mb = int(total_size / (1024 * 1024)) 193 - uptime_seconds = int(time.monotonic() - obs.start_at_mono) 193 + uptime_seconds = int(time.monotonic() - obs._start_mono) 194 194 195 195 self.stats = { 196 196 "captures_today": captures_today, ··· 408 408 # Segment timer 409 409 mins = segment_timer // 60 410 410 secs = segment_timer % 60 411 - self._segment_item.label = f"segment: {mins}:{secs:02d} remaining" 412 - self.menu.update_item(self._segment_item) 411 + new_label = f"segment: {mins}:{secs:02d} remaining" 412 + if self._segment_item.label != new_label: 413 + self._segment_item.label = new_label 414 + self.menu.update_item(self._segment_item) 413 415 414 416 # Stats (computed in update()) 415 417 if self.stats: ··· 418 420 synced_days = self.stats.get("synced_days", 0) 419 421 uptime = self.stats.get("uptime_seconds", 0) 420 422 421 - self._cache_item.label = f"cache: {size_mb} MB ({synced_days} days synced)" 422 - self._captures_item.label = f"captures today: {captures} segments" 423 + new_cache = f"cache: {size_mb} MB ({synced_days} days synced)" 424 + new_captures = f"captures today: {captures} segments" 423 425 424 426 hours = uptime // 3600 425 427 mins_up = (uptime % 3600) // 60 426 - self._uptime_item.label = f"uptime: {hours}h {mins_up}m" 428 + new_uptime = f"uptime: {hours}h {mins_up}m" 427 429 428 - self.menu.update_item(self._cache_item) 429 - self.menu.update_item(self._captures_item) 430 - self.menu.update_item(self._uptime_item) 430 + if self._cache_item.label != new_cache: 431 + self._cache_item.label = new_cache 432 + self.menu.update_item(self._cache_item) 433 + if self._captures_item.label != new_captures: 434 + self._captures_item.label = new_captures 435 + self.menu.update_item(self._captures_item) 436 + if self._uptime_item.label != new_uptime: 437 + self._uptime_item.label = new_uptime 438 + self.menu.update_item(self._uptime_item) 431 439 432 440 # Update pause remaining in resume button 433 441 if self.status == "paused" and pause_remaining > 0: 434 442 pr_mins = pause_remaining // 60 435 - self._resume_item.label = f"resume ({pr_mins}m remaining)" 436 - self.menu.update_item(self._resume_item) 443 + new_resume = f"resume ({pr_mins}m remaining)" 444 + if self._resume_item.label != new_resume: 445 + self._resume_item.label = new_resume 446 + self.menu.update_item(self._resume_item) 437 447 438 448 def _build_tooltip(self) -> str: 439 449 """Build rich tooltip body (HTML on KDE)."""
+1
tests/test_dbus_service.py
··· 39 39 observer.interval = 300 40 40 observer.segment_dir = None 41 41 observer.start_at_mono = time.monotonic() 42 + observer._start_mono = time.monotonic() 42 43 observer.stream = "test-stream" 43 44 observer._sync = None 44 45 observer._dbus_service = None
+19
tests/test_tray.py
··· 23 23 observer.segment_dir = None 24 24 observer.interval = 300 25 25 observer.start_at_mono = time.monotonic() 26 + observer._start_mono = time.monotonic() 26 27 observer._sync = None 27 28 observer._dbus_service = None 28 29 bus = MagicMock() ··· 142 143 assert app._cache_item.label == "cache: 42 MB (7 days synced)" 143 144 assert app._captures_item.label == "captures today: 5 segments" 144 145 assert app._uptime_item.label == "uptime: 2h 1m" 146 + 147 + def test_update_live_stats_skips_unchanged_menu_updates(self): 148 + app = _make_app() 149 + app._build_menu() 150 + app.menu.update_item = MagicMock() 151 + app.stats = { 152 + "captures_today": 5, 153 + "total_size_mb": 42, 154 + "synced_days": 7, 155 + "uptime_seconds": 7260, 156 + } 157 + 158 + app._update_live_stats(245, 0) 159 + app.menu.update_item.reset_mock() 160 + 161 + app._update_live_stats(245, 0) 162 + 163 + app.menu.update_item.assert_not_called() 145 164 146 165 147 166 class TestBuildTooltip: