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:
+75
-22
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user