""" Fleet Status Tab On-demand dashboard showing: - Overall health indicator - Docker containers status - Tunnel connectivity - Backup status - Disk usage Click "Refresh Now" button to check status (no automatic refresh). """ from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QGroupBox, QPushButton, QProgressBar, QListWidget ) from PyQt6.QtCore import QTimer, Qt from PyQt6.QtGui import QFont class FleetTab(QWidget): """Fleet monitoring dashboard tab""" def __init__(self): super().__init__() # Import database instead of FleetStatus # Fleet status is now cached in DB by daemon background jobs from core.database import DatabaseManager self.db = DatabaseManager() self._init_ui() # NO AUTOMATIC TIMERS - All checks are on-demand only # User must click "Refresh Now" button # Data comes from database (populated by daemon background jobs) # This keeps GUI fast and responsive (reads from DB in milliseconds) def _init_ui(self): """Create UI layout""" layout = QVBoxLayout() self.setLayout(layout) # Title title = QLabel("🚢 FLOTTA AUTONOMA - Status Monitor") title_font = QFont() title_font.setPointSize(18) title_font.setBold(True) title.setFont(title_font) layout.addWidget(title) # Overall status indicator self.overall_status = QLabel("Status: Not checked yet - Click Refresh to check") status_font = QFont() status_font.setPointSize(14) self.overall_status.setFont(status_font) self.overall_status.setStyleSheet("color: gray;") layout.addWidget(self.overall_status) # Stats boxes in row stats_row = QHBoxLayout() # Docker box docker_group = QGroupBox("🐳 Docker Containers") docker_layout = QVBoxLayout() self.docker_label = QLabel("Not checked") self.docker_details = QListWidget() self.docker_details.setMaximumHeight(150) docker_layout.addWidget(self.docker_label) docker_layout.addWidget(self.docker_details) docker_group.setLayout(docker_layout) stats_row.addWidget(docker_group) # Tunnel box tunnel_group = QGroupBox("🔒 WireGuard Tunnel") tunnel_layout = QVBoxLayout() self.tunnel_label = QLabel("Not checked") self.tunnel_latency = QLabel("") self.tunnel_packet_loss = QLabel("") tunnel_layout.addWidget(self.tunnel_label) tunnel_layout.addWidget(self.tunnel_latency) tunnel_layout.addWidget(self.tunnel_packet_loss) tunnel_group.setLayout(tunnel_layout) stats_row.addWidget(tunnel_group) layout.addLayout(stats_row) # Backup and Disk row backup_disk_row = QHBoxLayout() # Backup box backup_group = QGroupBox("💾 Restic Backup") backup_layout = QVBoxLayout() self.backup_label = QLabel("Not checked") self.backup_age = QLabel("") self.backup_count = QLabel("") backup_layout.addWidget(self.backup_label) backup_layout.addWidget(self.backup_age) backup_layout.addWidget(self.backup_count) backup_group.setLayout(backup_layout) backup_disk_row.addWidget(backup_group) # Disk box disk_group = QGroupBox("📊 Disk Space") disk_layout = QVBoxLayout() self.disk_ssd = QProgressBar() self.disk_hdd = QProgressBar() self.disk_root = QProgressBar() self.disk_ssd_label = QLabel("Hub SSD: --") self.disk_hdd_label = QLabel("Cassaforte HDD: --") self.disk_root_label = QLabel("Root: --") disk_layout.addWidget(self.disk_ssd_label) disk_layout.addWidget(self.disk_ssd) disk_layout.addWidget(self.disk_hdd_label) disk_layout.addWidget(self.disk_hdd) disk_layout.addWidget(self.disk_root_label) disk_layout.addWidget(self.disk_root) disk_group.setLayout(disk_layout) backup_disk_row.addWidget(disk_group) layout.addLayout(backup_disk_row) # Refresh button and last update button_row = QHBoxLayout() refresh_btn = QPushButton("🔄 Refresh Now") refresh_btn.clicked.connect(self.refresh) button_row.addWidget(refresh_btn) self.last_update_label = QLabel("Last update: Never") button_row.addWidget(self.last_update_label) button_row.addStretch() layout.addLayout(button_row) layout.addStretch() def refresh(self): """Refresh fleet status from database (instant).""" try: # Get latest status from database (instant query) status_db = self.db.get_latest_fleet_status() # Check if data is stale overall_db = status_db.get('overall', {}) age_seconds = overall_db.get('age_seconds') if age_seconds is None: self.overall_status.setText("⚠️ No data available - Daemon not running?") self.overall_status.setStyleSheet("color: gray;") return if age_seconds > 600: # 10 minutes self.overall_status.setText(f"⚠️ Data stale ({age_seconds // 60} min old)") self.overall_status.setStyleSheet("color: orange;") # Convert DB format to old format for compatibility status = self._convert_db_to_status(status_db) # Update overall status overall = status['overall'] if overall == 'healthy': self.overall_status.setText("Status: 🟢 All Systems Operational") self.overall_status.setStyleSheet("color: green;") elif overall == 'warning': self.overall_status.setText("Status: 🟡 Warning - Check Details") self.overall_status.setStyleSheet("color: orange;") else: self.overall_status.setText("Status: 🔴 Critical - Action Required") self.overall_status.setStyleSheet("color: red;") # Update Docker docker = status['docker'] self.docker_label.setText(docker.get('message', 'Unknown')) self.docker_details.clear() if 'details' in docker: for container in docker['details'][:10]: # Show first 10 status_emoji = '✓' if container['state'] == 'running' else '✗' self.docker_details.addItem(f"{status_emoji} {container['name']}") # Update Tunnel tunnel = status['tunnel'] self.tunnel_label.setText(tunnel.get('message', 'Unknown')) if tunnel.get('connected'): self.tunnel_latency.setText(f"Latency: {tunnel.get('latency_ms', 0):.1f}ms") self.tunnel_packet_loss.setText(f"Packet loss: {tunnel.get('packet_loss', 0)}%") else: self.tunnel_latency.setText("Disconnected") self.tunnel_packet_loss.setText("") # Update Backup backup = status['backup'] self.backup_label.setText(backup.get('message', 'Unknown')) if 'last_snapshot_date' in backup: self.backup_age.setText(f"Last: {backup['last_snapshot_date']}") if 'snapshot_count' in backup: self.backup_count.setText(f"Total snapshots: {backup['snapshot_count']}") # Update Disk disk = status['disk'] if 'disks' in disk: for d in disk['disks']: percent = int(d['used_percent']) free_gb = d.get('free_gb', 0) # Color code based on status if d['status'] == 'critical': style = "QProgressBar::chunk { background-color: red; }" elif d['status'] == 'warning': style = "QProgressBar::chunk { background-color: orange; }" else: style = "QProgressBar::chunk { background-color: green; }" if d['name'] == 'Hub SSD': self.disk_ssd.setValue(percent) self.disk_ssd.setStyleSheet(style) self.disk_ssd_label.setText( f"Hub SSD: {percent}% ({free_gb}GB free)" ) elif d['name'] == 'Cassaforte HDD': self.disk_hdd.setValue(percent) self.disk_hdd.setStyleSheet(style) self.disk_hdd_label.setText( f"Cassaforte HDD: {percent}% ({free_gb}GB free)" ) elif d['name'] == 'Root': self.disk_root.setValue(percent) self.disk_root.setStyleSheet(style) self.disk_root_label.setText( f"Root: {percent}% ({free_gb}GB free)" ) # Update timestamp with age from datetime import datetime check_time = overall_db.get('check_time') if check_time: dt = datetime.fromisoformat(check_time) self.last_update_label.setText( f"Last update: {dt.strftime('%H:%M:%S')} ({age_seconds}s ago)" ) else: self.last_update_label.setText("Last update: Never") except Exception as e: self.overall_status.setText(f"Error: {e}") self.overall_status.setStyleSheet("color: red;") print(f"Fleet tab refresh error: {e}") def _convert_db_to_status(self, status_db: dict) -> dict: """Convert database format to old FleetStatus format for compatibility. Args: status_db: Data from get_latest_fleet_status() Returns: dict: Status in old format """ docker_data = status_db.get('docker', {}).get('data', []) tunnel_data = status_db.get('tunnel', {}).get('data', {}) backup_data = status_db.get('backup', {}).get('data', {}) disk_data = status_db.get('disk', {}).get('data', []) return { 'overall': status_db.get('overall', {}).get('status', 'unknown'), 'docker': { 'status': status_db.get('docker', {}).get('status', 'unknown'), 'message': status_db.get('docker', {}).get('message', 'No data'), 'details': docker_data if docker_data else [] }, 'tunnel': { 'status': status_db.get('tunnel', {}).get('status', 'unknown'), 'message': status_db.get('tunnel', {}).get('message', 'No data'), 'connected': tunnel_data.get('connected', False) if tunnel_data else False, 'latency_ms': tunnel_data.get('latency_ms', 0) if tunnel_data else 0, 'packet_loss': tunnel_data.get('packet_loss', 0) if tunnel_data else 0 }, 'backup': { 'status': status_db.get('backup', {}).get('status', 'unknown'), 'message': status_db.get('backup', {}).get('message', 'No data'), 'snapshot_count': backup_data.get('snapshot_count', 0) if backup_data else 0, 'last_snapshot_date': backup_data.get('last_snapshot_date', 'never') if backup_data else 'never' }, 'disk': { 'status': status_db.get('disk', {}).get('status', 'unknown'), 'message': status_db.get('disk', {}).get('message', 'No data'), 'disks': disk_data if disk_data else [] } }