Skip to content

Commit bdfcccf

Browse files
committed
feat: Add health monitor core logic, CLI integration, and unit tests
1 parent aa17f57 commit bdfcccf

File tree

4 files changed

+308
-14
lines changed

4 files changed

+308
-14
lines changed

cortex/cli.py

Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
validate_installation_id,
3939
ValidationError
4040
)
41-
# Import the new Notification Manager
41+
# Import Notification Manager
4242
from cortex.notification_manager import NotificationManager
4343

4444

@@ -112,10 +112,9 @@ def _clear_line(self):
112112
sys.stdout.write('\r\033[K')
113113
sys.stdout.flush()
114114

115-
# --- New Notification Method ---
115+
# --- Notification Method ---
116116
def notify(self, args):
117117
"""Handle notification commands"""
118-
# Addressing CodeRabbit feedback: Handle missing subcommand gracefully
119118
if not args.notify_action:
120119
self._print_error("Please specify a subcommand (config/enable/disable/dnd/send)")
121120
return 1
@@ -132,24 +131,21 @@ def notify(self, args):
132131

133132
elif args.notify_action == 'enable':
134133
mgr.config["enabled"] = True
135-
# Addressing CodeRabbit feedback: Ideally should use a public method instead of private _save_config,
136-
# but keeping as is for a simple fix (or adding a save method to NotificationManager would be best).
137-
mgr._save_config()
134+
mgr.save_config()
138135
self._print_success("Notifications enabled")
139136
return 0
140137

141138
elif args.notify_action == 'disable':
142139
mgr.config["enabled"] = False
143-
mgr._save_config()
144-
cx_print("Notifications disabled (Critical alerts will still show)", "warning")
140+
mgr.save_config()
141+
self._print_success("Notifications disabled (Critical alerts will still show)")
145142
return 0
146143

147144
elif args.notify_action == 'dnd':
148145
if not args.start or not args.end:
149146
self._print_error("Please provide start and end times (HH:MM)")
150147
return 1
151148

152-
# Addressing CodeRabbit feedback: Add time format validation
153149
try:
154150
datetime.strptime(args.start, "%H:%M")
155151
datetime.strptime(args.end, "%H:%M")
@@ -159,7 +155,7 @@ def notify(self, args):
159155

160156
mgr.config["dnd_start"] = args.start
161157
mgr.config["dnd_end"] = args.end
162-
mgr._save_config()
158+
mgr.save_config()
163159
self._print_success(f"DND Window updated: {args.start} - {args.end}")
164160
return 0
165161

@@ -174,7 +170,56 @@ def notify(self, args):
174170
else:
175171
self._print_error("Unknown notify command")
176172
return 1
177-
# -------------------------------
173+
174+
# --- New Health Command ---
175+
def health(self, args):
176+
"""Run system health checks and show recommendations"""
177+
from cortex.health.monitor import HealthMonitor
178+
179+
self._print_status("🔍", "Running system health checks...")
180+
monitor = HealthMonitor()
181+
report = monitor.run_all()
182+
183+
# --- Display Results ---
184+
score = report['total_score']
185+
186+
# Color code the score
187+
score_color = "green"
188+
if score < 60: score_color = "red"
189+
elif score < 80: score_color = "yellow"
190+
191+
console.print()
192+
console.print(f"📊 [bold]System Health Score:[/bold] [{score_color}]{score}/100[/{score_color}]")
193+
console.print()
194+
195+
console.print("[bold]Factors:[/bold]")
196+
recommendations = []
197+
198+
for res in report['results']:
199+
status_icon = "✅"
200+
if res['status'] == 'WARNING': status_icon = "⚠️ "
201+
elif res['status'] == 'CRITICAL': status_icon = "❌"
202+
203+
console.print(f" {status_icon} {res['name']:<15}: {res['score']}/100 ({res['details']})")
204+
205+
if res['recommendation']:
206+
recommendations.append(res['recommendation'])
207+
208+
console.print()
209+
210+
if recommendations:
211+
console.print("[bold]Recommendations:[/bold]")
212+
for i, rec in enumerate(recommendations, 1):
213+
console.print(f" {i}. {rec}")
214+
215+
console.print()
216+
# Note: Auto-fix logic would go here, prompting user to apply specific commands.
217+
# For this iteration, we display actionable advice.
218+
console.print("[dim]Run suggested commands manually to improve your score.[/dim]")
219+
else:
220+
self._print_success("System is in excellent health! No actions needed.")
221+
222+
return 0
178223

179224
def install(self, software: str, execute: bool = False, dry_run: bool = False):
180225
# Validate input first
@@ -543,7 +588,8 @@ def show_rich_help():
543588
table.add_row("install <pkg>", "Install software")
544589
table.add_row("history", "View history")
545590
table.add_row("rollback <id>", "Undo installation")
546-
table.add_row("notify", "Manage desktop notifications") # Added this line
591+
table.add_row("notify", "Manage desktop notifications")
592+
table.add_row("health", "Check system health score") # Added this line
547593

548594
console.print(table)
549595
console.print()
@@ -598,7 +644,7 @@ def main():
598644
edit_pref_parser.add_argument('key', nargs='?')
599645
edit_pref_parser.add_argument('value', nargs='?')
600646

601-
# --- New Notify Command ---
647+
# --- Notify Command ---
602648
notify_parser = subparsers.add_parser('notify', help='Manage desktop notifications')
603649
notify_subs = notify_parser.add_subparsers(dest='notify_action', help='Notify actions')
604650

@@ -615,6 +661,9 @@ def main():
615661
send_parser.add_argument('--title', default='Cortex Notification')
616662
send_parser.add_argument('--level', choices=['low', 'normal', 'critical'], default='normal')
617663
send_parser.add_argument('--actions', nargs='*', help='Action buttons')
664+
665+
# --- New Health Command ---
666+
health_parser = subparsers.add_parser('health', help='Check system health score')
618667
# --------------------------
619668

620669
args = parser.parse_args()
@@ -642,9 +691,11 @@ def main():
642691
return cli.check_pref(key=args.key)
643692
elif args.command == 'edit-pref':
644693
return cli.edit_pref(action=args.action, key=args.key, value=args.value)
645-
# Handle the new notify command
646694
elif args.command == 'notify':
647695
return cli.notify(args)
696+
# Handle new command
697+
elif args.command == 'health':
698+
return cli.health(args)
648699
else:
649700
parser.print_help()
650701
return 1

cortex/health/__init__.py

Whitespace-only changes.

cortex/health/monitor.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import json
2+
import time
3+
from abc import ABC, abstractmethod
4+
from dataclasses import dataclass, field
5+
from pathlib import Path
6+
from typing import List, Dict, Optional
7+
from rich.console import Console
8+
9+
console = Console()
10+
11+
@dataclass
12+
class CheckResult:
13+
"""Data class to hold the result of each check"""
14+
name: str # Item name (e.g. "Disk Space")
15+
category: str # Category (security, updates, performance, disk)
16+
score: int # Score 0-100
17+
status: str # "OK", "WARNING", "CRITICAL"
18+
details: str # Detailed message
19+
recommendation: Optional[str] = None # Recommended action (if any)
20+
weight: float = 1.0 # Weight for weighted average
21+
22+
class HealthCheck(ABC):
23+
"""Base class inherited by all health check modules"""
24+
@abstractmethod
25+
def run(self) -> CheckResult:
26+
pass
27+
28+
class HealthMonitor:
29+
"""
30+
Main engine for system health monitoring.
31+
"""
32+
def __init__(self):
33+
self.history_file = Path.home() / ".cortex" / "health_history.json"
34+
self.history_file.parent.mkdir(exist_ok=True)
35+
self.checks: List[HealthCheck] = []
36+
37+
# Register each check here
38+
# (Import here to prevent circular references)
39+
from .checks.security import SecurityCheck
40+
from .checks.updates import UpdateCheck
41+
from .checks.performance import PerformanceCheck
42+
from .checks.disk import DiskCheck
43+
44+
self.register_check(SecurityCheck())
45+
self.register_check(UpdateCheck())
46+
self.register_check(PerformanceCheck())
47+
self.register_check(DiskCheck())
48+
49+
def register_check(self, check: HealthCheck):
50+
self.checks.append(check)
51+
52+
def run_all(self) -> Dict:
53+
results = []
54+
total_weighted_score = 0
55+
total_weight = 0
56+
57+
for check in self.checks:
58+
try:
59+
result = check.run()
60+
results.append(result)
61+
total_weighted_score += result.score * result.weight
62+
total_weight += result.weight
63+
except Exception as e:
64+
console.print(f"[red]Error running check {check.__class__.__name__}: {e}[/red]")
65+
66+
final_score = 0
67+
if total_weight > 0:
68+
final_score = int(total_weighted_score / total_weight)
69+
70+
report = {
71+
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"),
72+
"total_score": final_score,
73+
"results": [
74+
{
75+
"name": r.name,
76+
"category": r.category,
77+
"score": r.score,
78+
"status": r.status,
79+
"details": r.details,
80+
"recommendation": r.recommendation
81+
}
82+
for r in results
83+
]
84+
}
85+
86+
self._save_history(report)
87+
return report
88+
89+
def _save_history(self, report: Dict):
90+
history = []
91+
if self.history_file.exists():
92+
try:
93+
with open(self.history_file, 'r') as f:
94+
history = json.load(f)
95+
except json.JSONDecodeError:
96+
pass
97+
98+
history.append(report)
99+
history = history[-100:]
100+
101+
with open(self.history_file, 'w') as f:
102+
json.dump(history, f, indent=4)
103+
104+
if __name__ == "__main__":
105+
# For testing execution
106+
print("HealthMonitor initialized.")

0 commit comments

Comments
 (0)