@@ -6,6 +6,7 @@ Command-line interface for system health checking and monitoring via API endpoin
66
77import asyncio
88import json
9+ import re
910import sys
1011from typing import Any
1112
@@ -28,6 +29,116 @@ from app.services.system.ui import get_status_color_name, get_status_icon
2829app = typer.Typer(name="health", help=lazy_t("health.help"))
2930console = Console()
3031
32+ # Pattern-based translation for health API response messages.
33+ # Maps English substrings/prefixes to i18n keys for display-time translation.
34+ _HEALTH_MSG_EXACT: dict[str, str] = {
35+ "Aegis Stack application": "health.msg.aegis_ok",
36+ "Aegis Stack has issues": "health.msg.aegis_issues",
37+ "Some components have issues": "health.msg.components_issues",
38+ "Some services have issues": "health.msg.services_issues",
39+ "System container metrics": "health.msg.system_ok",
40+ "System container has issues": "health.msg.system_issues",
41+ "Database connection successful": "health.msg.db_ok",
42+ "Database module not available": "health.msg.db_module_missing",
43+ "PostgreSQL server not reachable": "health.msg.db_not_reachable",
44+ "PostgreSQL authentication failed": "health.msg.db_auth_failed",
45+ "PostgreSQL database does not exist": "health.msg.db_not_exist",
46+ "Database not initialized - file does not exist": "health.msg.db_not_init",
47+ "Database file not accessible": "health.msg.db_not_accessible",
48+ "Redis cache connection and operations successful": "health.msg.cache_ok",
49+ "Cache library not installed": "health.msg.cache_not_installed",
50+ "No active workers": "health.msg.no_active_workers",
51+ "idle": "health.msg.idle",
52+ "configured - no functions defined": "health.msg.no_functions",
53+ "Auth service configured and ready": "health.msg.auth_ok",
54+ "AI service is disabled": "health.msg.ai_disabled",
55+ "No communication providers configured": "health.msg.comms_none",
56+ "Communications service fully configured": "health.msg.comms_ok",
57+ "Ollama server not reachable": "health.msg.ollama_not_reachable",
58+ "Ollama running but no models installed": "health.msg.ollama_no_models",
59+ "worker offline - no health check data": "health.msg.worker_offline",
60+ }
61+
62+ _HEALTH_MSG_PATTERNS: list[tuple[re.Pattern[str], str, str]] = [
63+ # (compiled_regex, i18n_key, group_mapping)
64+ (re.compile(r"^(\d+) components available$"), "health.msg.components_ok", "count"),
65+ (re.compile(r"^(\d+) services available$"), "health.msg.services_ok", "count"),
66+ (re.compile(r"^Memory usage: ([\d.]+)%$"), "health.msg.memory_usage", "pct"),
67+ (re.compile(r"^Disk usage: ([\d.]+)%$"), "health.msg.disk_usage", "pct"),
68+ (re.compile(r"^CPU usage: ([\d.]+)%$"), "health.msg.cpu_usage", "pct"),
69+ (re.compile(r"^Scheduler running with (\d+) tasks?$"), "health.msg.scheduler_running", "count"),
70+ (re.compile(r"^Database connection failed"), "health.msg.db_failed", ""),
71+ (re.compile(r"^FastAPI backend active"), "health.msg.backend_active", ""),
72+ (re.compile(r"^(arq|TaskIQ|Dramatiq) worker infrastructure"), "health.msg.worker_infra", "backend"),
73+ (re.compile(r"^(\d+)/(\d+) workers? active$"), "health.msg.workers_active", "active,total"),
74+ (re.compile(r"^(\d+) functional queues configured"), "health.msg.queues_configured", "count"),
75+ (re.compile(r"^\((\d+) (?:active|with consumers|with heartbeats)\)$"), "health.msg.queues_active", "count"),
76+ (re.compile(r"^(\d+) processing$"), "health.msg.processing", "count"),
77+ (re.compile(r"^(\d+) queued$"), "health.msg.queued", "count"),
78+ (re.compile(r"^(\d+) completed$"), "health.msg.completed", "count"),
79+ (re.compile(r"^(\d+) failed \(([\d.]+)%\)$"), "health.msg.failed", "count,pct"),
80+ (re.compile(r"^AI service ready"), "health.msg.ai_ready", ""),
81+ (re.compile(r"^Comms service partially configured"), "health.msg.comms_partial", ""),
82+ ]
83+
84+
85+ _QUEUE_DESC_MAP: dict[str, str] = {
86+ "Load testing and performance testing": "health.msg.queue.load_test",
87+ "Image and file processing": "health.msg.queue.media",
88+ "System maintenance and monitoring tasks": "health.msg.queue.system",
89+ }
90+
91+
92+ def _translate_single_part(part: str) -> str:
93+ """Translate a single status part (used for comma-separated segments)."""
94+ key = _HEALTH_MSG_EXACT.get(part)
95+ if key:
96+ return t(key)
97+ for pattern, i18n_key, groups in _HEALTH_MSG_PATTERNS:
98+ match = pattern.match(part)
99+ if match:
100+ if not groups:
101+ return t(i18n_key)
102+ group_names = groups.split(",")
103+ kwargs = {name: match.group(i + 1) for i, name in enumerate(group_names)}
104+ return t(i18n_key, **kwargs)
105+ return part
106+
107+
108+ def _translate_health_msg(msg: str) -> str:
109+ """Translate a health API message at display time."""
110+ # Try exact match first
111+ key = _HEALTH_MSG_EXACT.get(msg)
112+ if key:
113+ return t(key)
114+
115+ # Try pattern matching
116+ for pattern, i18n_key, groups in _HEALTH_MSG_PATTERNS:
117+ match = pattern.match(msg)
118+ if match:
119+ if not groups:
120+ return t(i18n_key)
121+ group_names = groups.split(",")
122+ kwargs = {name: match.group(i + 1) for i, name in enumerate(group_names)}
123+ return t(i18n_key, **kwargs)
124+
125+ # Try composite "description: status" messages (e.g., queue messages)
126+ if ": " in msg:
127+ desc, status = msg.split(": ", 1)
128+ desc_key = _QUEUE_DESC_MAP.get(desc)
129+ # Translate each comma-separated status part individually
130+ status_parts = [p.strip() for p in status.split(", ")]
131+ translated_parts = []
132+ for part in status_parts:
133+ translated = _translate_single_part(part)
134+ translated_parts.append(translated)
135+ status = ", ".join(translated_parts)
136+ if desc_key:
137+ return f"{t(desc_key)}:{status}"
138+
139+ # No translation found — return original
140+ return msg
141+
31142
32143def _get_status_icon_and_color(status: ComponentStatusType) -> tuple[str, str]:
33144 """Get the appropriate icon and color for a component status (shared mapping)."""
@@ -296,7 +407,7 @@ def _display_sub_components(
296407 sub_line = f"{tree_connector}[{sub_color}]{sub_icon} {sub_name}[/{sub_color}]"
297408 if detailed and sub_component.response_time_ms is not None:
298409 sub_line += f" ([dim]{sub_component.response_time_ms:.1f}ms[/dim])"
299- sub_line += f" {sub_component.message}"
410+ sub_line += f" {_translate_health_msg( sub_component.message) }"
300411 console.print(sub_line)
301412
302413 # Recursively display sub-sub-components
@@ -463,7 +574,7 @@ def _display_health_status(
463574 component_line = f"[{status_color}]{status_icon} {name}[/{status_color}]"
464575 if detailed and component.response_time_ms is not None:
465576 component_line += f" ([dim]{component.response_time_ms:.1f}ms[/dim])"
466- component_line += f" {component.message}"
577+ component_line += f" {_translate_health_msg( component.message) }"
467578 console.print(component_line)
468579
469580 # Display sub-components with tree structure (recursive)
@@ -489,7 +600,11 @@ def _display_health_status(
489600 if system_info:
490601 sys_info_content = []
491602 for key, value in system_info.items():
492- sys_info_content.append(f"{key.replace('_', ' ').title()}: {value}")
603+ label_key = f"health.sysinfo.{key}"
604+ label = t(label_key)
605+ if label == label_key:
606+ label = key.replace("_", " ").title()
607+ sys_info_content.append(f"{label}:{value}")
493608
494609 console.print(
495610 Panel(
0 commit comments