Skip to content

Commit 26cce4c

Browse files
committed
fix: startup card showed Unraid System/Array red while running — yield before snapshot (v0.14.4)
1 parent 1abd60b commit 26cce4c

6 files changed

Lines changed: 97 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
All notable changes to UnraidMonitor will be documented in this file.
44

5+
## [0.14.4] - 2026-06-05
6+
7+
### Fixed
8+
- **Startup card no longer shows Unraid System/Array as red when they're actually running** — a timing race in the startup notification. Each monitor sets `is_running` synchronously at the top of its `start()` coroutine, but `asyncio.create_task()` only *schedules* that coroutine. The Unraid system/array monitors are created *after* `await client.connect()` with nothing yielding before the card is built, so their tasks hadn't run yet and rendered 🔴 (they were running fine moments later — `/health` always showed green). Also fixed the latent case where, with Unraid not configured, there's no `connect()` await at all and *every* monitor could show red. `_start_background_monitors` now yields one event-loop turn (`await asyncio.sleep(0)`) before returning, so all monitors report their real state when the card snapshots them.
9+
510
## [0.14.3] - 2026-06-05
611

712
### Changed

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ src/utils/version_store.py - read/write data/announced_version.json for startup
9292

9393
## Project Overview
9494

95-
Unraid Server Monitor Bot (v0.14.3) - A Docker-based Telegram bot for monitoring Unraid servers. Monitors Docker containers (events, logs, resources) and Unraid server health (CPU, memory, disks, array, UPS). Uses multi-provider LLM support (Anthropic, OpenAI, Ollama) for AI-powered diagnostics and natural language interaction. Sends alerts via Telegram with quick-action buttons.
95+
Unraid Server Monitor Bot (v0.14.4) - A Docker-based Telegram bot for monitoring Unraid servers. Monitors Docker containers (events, logs, resources) and Unraid server health (CPU, memory, disks, array, UPS). Uses multi-provider LLM support (Anthropic, OpenAI, Ollama) for AI-powered diagnostics and natural language interaction. Sends alerts via Telegram with quick-action buttons.
9696

9797
**Version string lives in TWO places**`pyproject.toml` (`version`) and `src/__init__.py` (`__version__`). Both must be bumped together on release; `tests/test_version.py` guards against drift. `BOT_VERSION` (in `src/bot/health_command.py`) resolves from installed package metadata when available, else falls back to `src.__version__` — the package is not installed in the Docker image, so the `__version__` fallback is what runs in production.
9898

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "unraid-monitor-bot"
3-
version = "0.14.3"
3+
version = "0.14.4"
44
requires-python = ">=3.11"
55
dependencies = [
66
"docker>=7.0.0,<8.0.0",

src/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.14.3"
1+
__version__ = "0.14.4"

src/startup.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,15 @@ async def _start_background_monitors(
335335
except Exception as send_err:
336336
logger.error(f"Failed to send Unraid error to {cid}: {send_err}")
337337

338+
# Yield once so every task created above runs its synchronous prefix and
339+
# flips is_running=True before the startup card snapshots monitor state.
340+
# Each monitor's start() sets the flag before its first await, so a single
341+
# event-loop turn is enough. Without this the Unraid system/array monitors
342+
# (created after the connect() await, with nothing yielding afterwards) — and
343+
# all monitors when Unraid is not configured (no connect() await at all) —
344+
# would render red on the startup card despite running fine moments later.
345+
await asyncio.sleep(0)
346+
338347

339348
async def start_monitoring(
340349
config: AppConfig,
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""Regression test: monitors report is_running before the startup card snapshots them.
2+
3+
The startup card is built immediately after `_start_background_monitors` returns,
4+
with no further yield. Each monitor sets `_running=True` synchronously at the top
5+
of its `start()` coroutine, but `create_task` only schedules that coroutine — it
6+
doesn't run it. So `_start_background_monitors` must yield once before returning,
7+
or freshly-created monitors render red on the card despite running fine.
8+
"""
9+
10+
import asyncio
11+
from unittest.mock import MagicMock
12+
13+
import src.startup as startup_mod
14+
from src.background import _BackgroundTasks
15+
16+
17+
class _FakeMonitor:
18+
"""Mimics a real monitor: flips _running before its first await, then loops."""
19+
20+
def __init__(self) -> None:
21+
self._running = False
22+
23+
@property
24+
def is_running(self) -> bool:
25+
return self._running
26+
27+
async def start(self) -> None:
28+
self._running = True
29+
while self._running:
30+
await asyncio.sleep(3600)
31+
32+
33+
class _FakeClient:
34+
async def connect(self) -> None:
35+
return None
36+
37+
38+
class _FakeUnraid:
39+
def __init__(self, client, system_monitor, array_monitor) -> None:
40+
self.client = client
41+
self.system_monitor = system_monitor
42+
self.array_monitor = array_monitor
43+
44+
45+
async def _cancel(bg: _BackgroundTasks) -> None:
46+
for task in bg._tasks:
47+
task.cancel()
48+
await asyncio.gather(*bg._tasks, return_exceptions=True)
49+
50+
51+
async def test_unraid_monitors_running_when_card_is_built():
52+
"""The reported bug: Unraid system/array created after connect() showed red."""
53+
bg = _BackgroundTasks()
54+
bg.monitor = _FakeMonitor()
55+
bg.log_watcher = _FakeMonitor()
56+
uc = _FakeUnraid(_FakeClient(), _FakeMonitor(), _FakeMonitor())
57+
58+
await startup_mod._start_background_monitors(bg, None, uc, MagicMock(), MagicMock())
59+
try:
60+
assert bg.monitor.is_running
61+
assert bg.log_watcher.is_running
62+
assert uc.system_monitor.is_running
63+
assert uc.array_monitor.is_running
64+
finally:
65+
await _cancel(bg)
66+
67+
68+
async def test_core_monitors_running_when_unraid_not_configured():
69+
"""The latent case: with no Unraid client there's no connect() await at all."""
70+
bg = _BackgroundTasks()
71+
bg.monitor = _FakeMonitor()
72+
bg.log_watcher = _FakeMonitor()
73+
uc = _FakeUnraid(None, None, None)
74+
75+
await startup_mod._start_background_monitors(bg, None, uc, MagicMock(), MagicMock())
76+
try:
77+
assert bg.monitor.is_running
78+
assert bg.log_watcher.is_running
79+
finally:
80+
await _cancel(bg)

0 commit comments

Comments
 (0)