Skip to content

Commit 6b87672

Browse files
committed
feat: show per-container memory on kill buttons + memory freed on stop (v0.17.0)
Memory-pressure warning/critical alerts now show each killable container's RAM usage on its Stop button and in the alert text. Stopping a container (button or auto-kill) reports how much it was using and the system memory level afterwards. MemoryMonitor reads stats itself for just the killable containers (get_killable_memory, bounded by stats_timeout), so it works even when resource monitoring is disabled; kill_container returns KillResult. Claude-Session: https://claude.ai/code/session_013fuQC7uuGuYTK8M3Jp5FpS
1 parent e794358 commit 6b87672

13 files changed

Lines changed: 371 additions & 72 deletions

.claude/structure/monitors.yaml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,10 @@ files:
174174
kind: enum
175175
description: System memory pressure state (NORMAL, WARNING, CRITICAL, KILLING, RECOVERING)
176176

177+
- name: KillResult
178+
kind: dataclass
179+
description: Outcome of stopping a container (success, name, freed_bytes, system_percent, system_available_bytes) for user feedback
180+
177181
- name: MemoryMonitor
178182
kind: class
179183
description: Monitors system memory, sends escalated alerts, kills/restarts containers to manage memory pressure
@@ -184,9 +188,12 @@ files:
184188
- name: get_memory_percent
185189
params: []
186190
returns: float
191+
- name: get_killable_memory
192+
params: []
193+
returns: Awaitable[list[tuple[str, int | None]]]
187194
- name: kill_container
188195
params: [{name: name, type: str}]
189-
returns: Awaitable[bool]
196+
returns: Awaitable[KillResult]
190197
- name: cancel_pending_kill
191198
params: []
192199
returns: bool

CHANGELOG.md

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

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

5+
## [0.17.0] - 2026-06-28
6+
7+
### Added
8+
- **Per-container memory on the kill buttons** — the memory-pressure warning/critical alerts now show how much RAM each killable container is using right on its "⏹ Stop" button (and in the alert text), so you can free the most memory first. `MemoryMonitor` now reads this itself for just the killable containers (`get_killable_memory`), so it works even when resource monitoring is disabled and is robust if Docker is slow under pressure (bounded by `stats_timeout`, falls back to names-only).
9+
- **Memory feedback when a container is stopped** — stopping a container (via button or the auto-kill countdown) now reports how much memory it was using and the system memory level afterwards (e.g. "It was using ~1.8GB. System memory now 78% (6.2GB free)."). `kill_container` returns a new `KillResult` carrying this context.
10+
11+
### Changed
12+
- The memory alert handler no longer depends on the resource monitor for button labels — memory figures are supplied by `MemoryMonitor` through the alert itself.
13+
- The auto-kill confirmation reads system memory *after* the stop (it previously reported the pre-kill figure).
14+
515
## [0.16.1] - 2026-06-25
616

717
### Fixed

CLAUDE.md

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

9494
## Project Overview
9595

96-
Unraid Server Monitor Bot (v0.16.1) - 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.
96+
Unraid Server Monitor Bot (v0.17.0) - 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.
9797

9898
**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.
9999

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.16.1"
3+
version = "0.17.0"
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.16.1"
1+
__version__ = "0.17.0"

src/bot/alert_callbacks.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from src.state import ContainerStateManager
2727
from src.services.container_control import ContainerController
2828
from src.services.diagnostic import DiagnosticService
29-
from src.utils.formatting import validate_container_name, escape_markdown, truncate_callback_data, format_duration_minutes
29+
from src.utils.formatting import validate_container_name, escape_markdown, truncate_callback_data, format_duration_minutes, format_bytes
3030
from src.utils.sanitize import sanitize_logs_for_display
3131
from src.utils.telegram_format import markdown_to_telegram_html
3232

@@ -384,13 +384,23 @@ async def handler(callback: CallbackQuery) -> None:
384384

385385
await callback.answer(f"Stopping {container_name}...")
386386

387-
success = await memory_monitor.kill_container(container_name)
387+
result = await memory_monitor.kill_container(container_name)
388388

389389
if callback.message:
390-
if success:
391-
await callback.message.answer(
392-
f"⏹ Stopped *{escape_markdown(container_name)}* to free memory.", parse_mode="Markdown"
393-
)
390+
if result.success:
391+
lines = [f"⏹ Stopped *{escape_markdown(container_name)}* to free memory."]
392+
if result.freed_bytes:
393+
lines.append(f"It was using ~{format_bytes(result.freed_bytes)}.")
394+
if result.system_percent is not None:
395+
free = (
396+
format_bytes(result.system_available_bytes)
397+
if result.system_available_bytes is not None
398+
else "?"
399+
)
400+
lines.append(
401+
f"System memory now {result.system_percent:.0f}% ({free} free)."
402+
)
403+
await callback.message.answer(" ".join(lines), parse_mode="Markdown")
394404
else:
395405
await callback.message.answer(
396406
f"❌ Failed to stop {container_name}. It may already be stopped."

src/constants.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,10 @@
111111
# Shown once when BOT_VERSION first differs from data/announced_version.json.
112112
ANNOUNCED_VERSION_PATH = "data/announced_version.json"
113113
WHATS_NEW: dict[str, list[str]] = {
114+
"0.17.0": [
115+
"Memory pressure alerts now show how much RAM each container is using right on the Stop buttons, so you can free the most memory first",
116+
"When a container is stopped to free memory, the bot tells you how much it was using and how much system memory is now free",
117+
],
114118
"0.16.0": [
115119
"Health check in the Unraid dashboard - the container now reports healthy/unhealthy in docker ps and the Unraid UI, no setup needed",
116120
"No more lost boot alerts - alerts queued before your first /start are retried if Telegram is flaky during delivery, instead of vanishing",

src/monitor_callbacks.py

Lines changed: 25 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from src.alerts.rate_limiter import RateLimiter
1616
from src.alert_proxy import AlertManagerProxy
1717
from src.monitors.resource_monitor import ResourceMonitor
18+
from src.utils.formatting import format_bytes
1819
from src.utils.telegram_retry import send_with_retry
1920

2021

@@ -155,11 +156,13 @@ async def on_server_alert(title: str, message: str, alert_type: str) -> None:
155156
def make_memory_alert_handler(
156157
chat_id_store: ChatIdStore,
157158
bot: Bot,
158-
resource_monitor: ResourceMonitor | None,
159159
escape_markdown_fn: Callable[[str], str],
160-
) -> Callable[[str, str, str, list[str]], Awaitable[None]]:
160+
) -> Callable[[str, str, str, list[tuple[str, int | None]]], Awaitable[None]]:
161161
async def on_memory_alert(
162-
title: str, message: str, alert_type: str, killable_names: list[str]
162+
title: str,
163+
message: str,
164+
alert_type: str,
165+
killable: list[tuple[str, int | None]],
163166
) -> None:
164167
chat_ids = chat_id_store.get_all_chat_ids()
165168
if not chat_ids:
@@ -169,37 +172,25 @@ async def on_memory_alert(
169172
alert_text = f"{emoji} *{escape_markdown_fn(title)}*\n\n{message}"
170173
keyboard = None
171174

172-
if alert_type in ("warning", "critical") and killable_names:
173-
# Try to get per-container memory stats for button labels
174-
stats_by_name: dict[str, str] = {}
175-
if resource_monitor is not None:
176-
try:
177-
all_stats = await asyncio.wait_for(
178-
resource_monitor.get_all_stats(), timeout=5.0
179-
)
180-
stats_by_name = {s.name: s.memory_display for s in all_stats}
181-
except Exception:
182-
pass # Graceful degradation -- buttons without memory info
183-
184-
if alert_type == "warning":
185-
buttons = []
186-
for name in killable_names:
187-
mem = stats_by_name.get(name, "")
188-
label = f"⏹ Stop {name}" + (f" ({mem})" if mem else "")
189-
buttons.append(
190-
[InlineKeyboardButton(text=label, callback_data=f"mem_kill:{name}")]
191-
)
192-
if buttons:
193-
keyboard = InlineKeyboardMarkup(inline_keyboard=buttons)
194-
195-
elif alert_type == "critical":
196-
target = killable_names[0]
197-
mem = stats_by_name.get(target, "")
198-
kill_label = f"⏹ Kill {target} Now" + (f" ({mem})" if mem else "")
199-
keyboard = InlineKeyboardMarkup(inline_keyboard=[
200-
[InlineKeyboardButton(text=kill_label, callback_data=f"mem_kill:{target}")],
201-
[InlineKeyboardButton(text="❌ Cancel Auto-Kill", callback_data="mem_cancel_kill")],
202-
])
175+
# Memory usage per container is supplied by the MemoryMonitor (it only
176+
# queries the killable containers, so this is robust even when the
177+
# resource monitor is disabled or Docker is slow under pressure).
178+
if alert_type == "warning" and killable:
179+
buttons = []
180+
for name, mem in killable:
181+
label = f"⏹ Stop {name}" + (f" ({format_bytes(mem)})" if mem else "")
182+
buttons.append(
183+
[InlineKeyboardButton(text=label, callback_data=f"mem_kill:{name}")]
184+
)
185+
keyboard = InlineKeyboardMarkup(inline_keyboard=buttons)
186+
187+
elif alert_type == "critical" and killable:
188+
target, mem = killable[0]
189+
kill_label = f"⏹ Kill {target} Now" + (f" ({format_bytes(mem)})" if mem else "")
190+
keyboard = InlineKeyboardMarkup(inline_keyboard=[
191+
[InlineKeyboardButton(text=kill_label, callback_data=f"mem_kill:{target}")],
192+
[InlineKeyboardButton(text="❌ Cancel Auto-Kill", callback_data="mem_cancel_kill")],
193+
])
203194

204195
for cid in chat_ids:
205196
try:

0 commit comments

Comments
 (0)