# Copyright (C) 2025 James Brotosky, Brandon Wickline # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import datetime import logging from bson import ObjectId from textual.app import ComposeResult from textual.containers import Container, Vertical from textual.widgets import Button, DataTable, Static from services.API import AirlockAPIWrapper logger = logging.getLogger(__name__) def skipback(days): """ Generate a MongoDB ObjectId for a given number of days ago from today. """ adjusted_days = days date_days_ago = datetime.datetime.now(datetime.UTC) - datetime.timedelta( days=adjusted_days ) timestamp = int(date_days_ago.timestamp()) hex_timestamp = format(timestamp, "08x") objectid_hex = hex_timestamp + "0000000000000000" return ObjectId(objectid_hex) class ServerLogWidget(Vertical): """Widget for displaying server activity logs in a DataTable.""" DEFAULT_CSS = """ ServerLogWidget { width: 100%; height: 100%; } ServerLogWidget #status_bar { width: 100%; height: auto; background: $surface; padding: 1; margin-bottom: 1; } ServerLogWidget DataTable { height: 1fr; border: solid $primary; } ServerLogWidget #button_container { width: 100%; height: auto; layout: horizontal; padding: 1; } ServerLogWidget Button { margin-right: 1; } """ def __init__(self, api: AirlockAPIWrapper): super().__init__() self.api = api def compose(self) -> ComposeResult: yield Static("Loading server logs (last 72 hours)...", id="status_bar") yield DataTable(id="server_log_table") with Container(id="button_container"): yield Button("Refresh", id="refresh_button", variant="primary") def on_mount(self) -> None: """Initialize the DataTable and load server logs.""" self.load_logs() def load_logs(self) -> None: """Load server logs from the API and populate the DataTable.""" table = self.query_one("#server_log_table", DataTable) status = self.query_one("#status_bar", Static) try: status.update("⏳ Loading server logs (last 72 hours)...") # Create a fake checkpoint for 3 days ago (72 hours) checkpoint = str(skipback(3)) # Get server logs from API logs = self.api.server_logs(checkpoint=checkpoint) if not logs: status.update("ℹï¸ No server logs found in the last 72 hours.") table.clear(columns=True) return # Clear existing data table.clear(columns=True) # Add columns based on the first log entry if logs: first_log = logs[0] columns = [col for col in first_log.keys() if col != "checkpoint"] for col in columns: table.add_column(col, key=col) # Add rows in reverse order so newest entries are at the top for log_entry in reversed(logs): row_data = [] for col in columns: value = log_entry.get(col, "") # Format datetime column to be more readable if col == "datetime" and value: try: # Parse ISO format and convert to readable format dt = datetime.datetime.fromisoformat( str(value).replace("Z", "+00:00") ) value = dt.strftime("%Y-%m-%d %H:%M:%S") except Exception: # If parsing fails, just use the original value pass row_data.append(str(value)) table.add_row(*row_data) status.update( f"✅ Loaded {len(logs)} log entries from the last 72 hours" ) logger.info(f"Loaded {len(logs)} server log entries") else: status.update("ℹï¸ No log entries found.") except Exception as exc: error_msg = f"❌ Error loading server logs: {exc}" status.update(error_msg) logger.error(f"Failed to load server logs: {exc}", exc_info=True) table.clear(columns=True) def on_button_pressed(self, event: Button.Pressed) -> None: """Handle button presses.""" button_id = event.button.id if button_id == "refresh_button": self.load_logs() event.stop()