feat: Multiple UI improvements and new server log functionality

- Add Server Log tab with DataTable display of server activity logs

- Fix keyboard navigation bug in agents tab

- Add execution history viewer for selected agents

- Improve policy tree widget functionality by adding single device operations

- Integrate logging notifications into TUI
  - Add TextualNotificationHandler to setup.py
  - Display ERROR/WARNING/CRITICAL logs as toast notifications
  - Remove terminal output to prevent interference with TUI
  - Logs still written to Loxide.log file

closes #45
This commit is contained in:
2025-12-16 16:16:49 -05:00
parent 57d0f12000
commit fc17c869fc
7 changed files with 1183 additions and 65 deletions
+75 -22
View File
@@ -29,6 +29,49 @@ from utils.configmanager import (
)
class TextualNotificationHandler(logging.Handler):
"""
Custom logging handler that sends ERROR, WARNING, and CRITICAL logs
to Textual toast notifications.
"""
def __init__(self, app):
super().__init__()
self.app = app
def emit(self, record):
try:
# Only handle ERROR, WARNING, and CRITICAL
if record.levelno >= logging.WARNING:
# Format the message
msg = self.format(record)
# Map log levels to Textual severity
severity_map = {
logging.WARNING: "warning",
logging.ERROR: "error",
logging.CRITICAL: "error",
}
severity = severity_map.get(record.levelno, "information")
# Send to Textual notification
# Use call_from_thread if logging from non-main thread
try:
self.app.notify(msg, severity=severity, timeout=5)
except Exception:
# If we're not on the main thread, schedule it
try:
self.app.call_from_thread(
self.app.notify, msg, severity=severity, timeout=5
)
except Exception:
# Silently fail to avoid breaking the logging system
pass
except Exception:
# Silently fail to avoid breaking the logging system
pass
def get_base_directory() -> Path:
system = platform.system()
home = Path.home()
@@ -44,38 +87,31 @@ def configure_logging(log_dir: Path, log_level: str = "INFO"):
log_file = log_dir / "Loxide.log"
config = {
"version": 1, # Required key for dictConfig format version
"disable_existing_loggers": False, # Keeps existing loggers active
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"detailed": {
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
# Includes timestamp, logger name, level, and message
},
"simple": {
"format": "%(levelname)s - %(message)s"
# Minimal format for console output
},
"simple": {"format": "%(levelname)s - %(message)s"},
"toast": {"format": "%(name)s: %(message)s"}, # Simpler format for toasts
},
"handlers": {
"file": {
"class": "logging.handlers.TimedRotatingFileHandler",
"filename": str(log_file),
"when": "midnight", # Rotate logs at midnight
"interval": 1, # Every 1 day
"backupCount": 7, # Keep 7 days of logs
"encoding": "utf-8", # Ensure UTF-8 encoding
"level": "DEBUG", # Always log DEBUG and above to file
"formatter": "detailed", # Use detailed format
},
"console": {
"class": "logging.StreamHandler",
"level": log_level.upper(), # System-configured level for console
"formatter": "simple", # Use simple format
"when": "midnight",
"interval": 1,
"backupCount": 7,
"encoding": "utf-8",
"level": "DEBUG",
"formatter": "detailed",
},
# REMOVED console handler - it interferes with Textual TUI
},
"root": {
"level": "DEBUG", # Root logger level
"handlers": ["file", "console"], # Attach both handlers
"level": "DEBUG",
"handlers": ["file"], # Only use file handler, not console
},
}
@@ -96,6 +132,18 @@ def configure_logging(log_dir: Path, log_level: str = "INFO"):
logging.config.dictConfig(config)
logging.getLogger().debug("✅ Logging configured.")
# Return a function to attach the notification handler once the app is created
def attach_notification_handler(app):
"""Attach the Textual notification handler to the root logger."""
handler = TextualNotificationHandler(app)
handler.setLevel(logging.WARNING) # Only WARNING and above
formatter = logging.Formatter("%(name)s: %(message)s")
handler.setFormatter(formatter)
logging.getLogger().addHandler(handler)
logging.getLogger().debug("✅ Textual notification handler attached.")
return attach_notification_handler
def setup():
"""
@@ -105,6 +153,9 @@ def setup():
3. Load user config (mutable, from user_config.json)
4. Configure logging
5. Set up .env with WORKING_DIR only
Returns:
attach_notification_handler: Function to attach notification handler to TUI app
"""
base_dir = get_base_directory()
dirs = {
@@ -122,7 +173,7 @@ def setup():
# Configure logging with system-defined log level
log_level = get_system_value("LOG_LEVEL", str, "INFO")
configure_logging(dirs["logs"], log_level)
attach_handler = configure_logging(dirs["logs"], log_level)
# Load user config (mutable)
load_user_config(dirs["config"])
@@ -145,7 +196,6 @@ def setup():
"Approved": [],
"Needs_Review": ["Review_First", "Review_Second", "HTML"],
"Preflight": ["HTML"],
"Archived": [],
}
for folder_name, subfolders in folders_structure.items():
@@ -158,3 +208,6 @@ def setup():
logging.debug(f"'{subfolder}' subfolder created at: {subfolder_path}")
logging.info("✅ Setup complete")
# Return the attach handler function
return attach_handler