"""Main application window.""" from PyQt6.QtWidgets import ( QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QSplitter, QListWidget, QListWidgetItem, QLabel, QPushButton, QGroupBox, QTextEdit, QStatusBar, QMessageBox, QProgressDialog ) from PyQt6.QtCore import Qt, QTimer from PyQt6.QtGui import QIcon, QColor from config import APP_NAME, APP_VERSION from services.api_client import APIClient, Gateway, Endpoint from services.vpn_manager import VPNManager from .login_dialog import LoginDialog class MainWindow(QMainWindow): """Main application window.""" def __init__(self): super().__init__() self.setWindowTitle(f"{APP_NAME} v{APP_VERSION}") self.setMinimumSize(900, 600) self.api_client: APIClient | None = None self.vpn_manager = VPNManager() self.current_gateway: Gateway | None = None self.current_endpoint: Endpoint | None = None self.current_connection_id: int | None = None self._setup_ui() self._setup_timers() # Show login dialog on start QTimer.singleShot(100, self._show_login) def _setup_ui(self): """Setup UI components.""" central = QWidget() self.setCentralWidget(central) layout = QVBoxLayout(central) # Toolbar toolbar = QHBoxLayout() self.user_label = QLabel("Not logged in") toolbar.addWidget(self.user_label) toolbar.addStretch() self.refresh_button = QPushButton("Refresh") self.refresh_button.clicked.connect(self._refresh_data) self.refresh_button.setEnabled(False) toolbar.addWidget(self.refresh_button) self.logout_button = QPushButton("Logout") self.logout_button.clicked.connect(self._logout) self.logout_button.setEnabled(False) toolbar.addWidget(self.logout_button) layout.addLayout(toolbar) # Main content splitter splitter = QSplitter(Qt.Orientation.Horizontal) # Gateway list gateway_group = QGroupBox("Gateways") gateway_layout = QVBoxLayout(gateway_group) self.gateway_list = QListWidget() self.gateway_list.currentItemChanged.connect(self._on_gateway_selected) gateway_layout.addWidget(self.gateway_list) splitter.addWidget(gateway_group) # Endpoint list endpoint_group = QGroupBox("Endpoints") endpoint_layout = QVBoxLayout(endpoint_group) self.endpoint_list = QListWidget() self.endpoint_list.currentItemChanged.connect(self._on_endpoint_selected) endpoint_layout.addWidget(self.endpoint_list) splitter.addWidget(endpoint_group) # Connection panel connection_group = QGroupBox("Connection") connection_layout = QVBoxLayout(connection_group) self.gateway_info = QLabel("Select a gateway") connection_layout.addWidget(self.gateway_info) self.endpoint_info = QLabel("Select an endpoint") connection_layout.addWidget(self.endpoint_info) self.status_label = QLabel("Status: Disconnected") self.status_label.setStyleSheet("font-weight: bold;") connection_layout.addWidget(self.status_label) connection_layout.addStretch() # Connect button button_layout = QHBoxLayout() self.connect_button = QPushButton("Connect") self.connect_button.setEnabled(False) self.connect_button.clicked.connect(self._toggle_connection) self.connect_button.setMinimumHeight(40) button_layout.addWidget(self.connect_button) connection_layout.addLayout(button_layout) # Log area self.log_area = QTextEdit() self.log_area.setReadOnly(True) self.log_area.setMaximumHeight(150) connection_layout.addWidget(self.log_area) splitter.addWidget(connection_group) # Set splitter sizes splitter.setSizes([200, 200, 400]) layout.addWidget(splitter) # Status bar self.status_bar = QStatusBar() self.setStatusBar(self.status_bar) self.status_bar.showMessage("Ready") def _setup_timers(self): """Setup periodic timers.""" # Refresh gateway status every 30 seconds self.status_timer = QTimer() self.status_timer.timeout.connect(self._refresh_gateway_status) self.status_timer.setInterval(30000) def _show_login(self): """Show login dialog.""" dialog = LoginDialog(self) while True: if dialog.exec() != LoginDialog.DialogCode.Accepted: self.close() return server, username, password = dialog.get_credentials() if not all([server, username, password]): dialog.show_error("Please fill all fields") continue # Try to connect self.api_client = APIClient(server) if self.api_client.login(username, password): dialog.accept() self._on_login_success(username) break else: dialog.show_error("Login failed. Check credentials.") def _on_login_success(self, username: str): """Handle successful login.""" self.user_label.setText(f"Logged in as: {username}") self.refresh_button.setEnabled(True) self.logout_button.setEnabled(True) self.status_bar.showMessage("Connected to server") self._log("Connected to server") # Load data self._refresh_data() # Start status timer self.status_timer.start() def _logout(self): """Logout and show login dialog.""" # Disconnect VPN if connected if self.vpn_manager.is_connected(): self.vpn_manager.disconnect() if self.api_client: self.api_client.logout() self.api_client.close() self.api_client = None # Clear UI self.gateway_list.clear() self.endpoint_list.clear() self.user_label.setText("Not logged in") self.refresh_button.setEnabled(False) self.logout_button.setEnabled(False) self.connect_button.setEnabled(False) self.status_timer.stop() self._log("Logged out") # Show login dialog QTimer.singleShot(100, self._show_login) def _refresh_data(self): """Refresh all data from server.""" if not self.api_client: return self._log("Refreshing data...") self.status_bar.showMessage("Refreshing...") # Load gateways gateways = self.api_client.get_gateways() self.gateway_list.clear() for gateway in gateways: item = QListWidgetItem() item.setText(f"{gateway.name}") item.setData(Qt.ItemDataRole.UserRole, gateway) # Set color based on status if gateway.is_online: item.setForeground(QColor("green")) item.setToolTip(f"Online - {gateway.location or 'No location'}") else: item.setForeground(QColor("gray")) item.setToolTip("Offline") self.gateway_list.addItem(item) self.status_bar.showMessage(f"Loaded {len(gateways)} gateways") self._log(f"Loaded {len(gateways)} gateways") def _refresh_gateway_status(self): """Refresh only gateway online status.""" if not self.api_client: return status_list = self.api_client.get_gateways_status() status_map = {s["id"]: s["is_online"] for s in status_list} for i in range(self.gateway_list.count()): item = self.gateway_list.item(i) gateway: Gateway = item.data(Qt.ItemDataRole.UserRole) if gateway.id in status_map: is_online = status_map[gateway.id] if is_online: item.setForeground(QColor("green")) else: item.setForeground(QColor("gray")) def _on_gateway_selected(self, current, previous): """Handle gateway selection.""" if not current: self.endpoint_list.clear() self.current_gateway = None self.gateway_info.setText("Select a gateway") return gateway: Gateway = current.data(Qt.ItemDataRole.UserRole) self.current_gateway = gateway self.gateway_info.setText( f"Gateway: {gateway.name}\n" f"Type: {gateway.router_type}\n" f"Status: {'Online' if gateway.is_online else 'Offline'}\n" f"Location: {gateway.location or 'N/A'}" ) # Load endpoints self._load_endpoints(gateway.id) def _load_endpoints(self, gateway_id: int): """Load endpoints for gateway.""" if not self.api_client: return endpoints = self.api_client.get_endpoints(gateway_id) self.endpoint_list.clear() for endpoint in endpoints: item = QListWidgetItem() app_name = endpoint.application_name or "Custom" item.setText(f"{endpoint.name} ({app_name})") item.setToolTip(f"{endpoint.internal_ip}:{endpoint.port} ({endpoint.protocol})") item.setData(Qt.ItemDataRole.UserRole, endpoint) self.endpoint_list.addItem(item) def _on_endpoint_selected(self, current, previous): """Handle endpoint selection.""" if not current: self.current_endpoint = None self.endpoint_info.setText("Select an endpoint") self.connect_button.setEnabled(False) return endpoint: Endpoint = current.data(Qt.ItemDataRole.UserRole) self.current_endpoint = endpoint self.endpoint_info.setText( f"Endpoint: {endpoint.name}\n" f"Address: {endpoint.internal_ip}:{endpoint.port}\n" f"Protocol: {endpoint.protocol.upper()}\n" f"Application: {endpoint.application_name or 'N/A'}" ) # Enable connect button if gateway is online if self.current_gateway and self.current_gateway.is_online: self.connect_button.setEnabled(True) else: self.connect_button.setEnabled(False) def _toggle_connection(self): """Toggle VPN connection.""" if self.vpn_manager.is_connected(): self._disconnect() else: self._connect() def _connect(self): """Establish VPN connection.""" if not self.api_client or not self.current_gateway or not self.current_endpoint: return self._log(f"Connecting to {self.current_endpoint.name}...") self.status_label.setText("Status: Connecting...") self.connect_button.setEnabled(False) # Request connection from server result = self.api_client.connect( self.current_gateway.id, self.current_endpoint.id ) if not result.get("success"): self._log(f"Error: {result.get('message')}") self.status_label.setText("Status: Connection Failed") self.connect_button.setEnabled(True) return self.current_connection_id = result.get("connection_id") vpn_config = result.get("vpn_config") if not vpn_config: self._log("Error: No VPN config received") self.status_label.setText("Status: Error") self.connect_button.setEnabled(True) return # Start VPN connection status = self.vpn_manager.connect(vpn_config) if status.connected: self._log("VPN connected!") self.status_label.setText("Status: Connected") self.status_label.setStyleSheet("font-weight: bold; color: green;") self.connect_button.setText("Disconnect") self.connect_button.setEnabled(True) self._log(f"Target: {result.get('target_ip')}:{result.get('target_port')}") else: self._log(f"VPN Error: {status.error}") self.status_label.setText("Status: VPN Failed") self.connect_button.setEnabled(True) def _disconnect(self): """Disconnect VPN.""" self._log("Disconnecting...") # Disconnect VPN self.vpn_manager.disconnect() # Notify server if self.api_client and self.current_connection_id: self.api_client.disconnect(self.current_connection_id) self.current_connection_id = None self.status_label.setText("Status: Disconnected") self.status_label.setStyleSheet("font-weight: bold; color: black;") self.connect_button.setText("Connect") self._log("Disconnected") def _log(self, message: str): """Add message to log area.""" from datetime import datetime timestamp = datetime.now().strftime("%H:%M:%S") self.log_area.append(f"[{timestamp}] {message}") def closeEvent(self, event): """Handle window close.""" # Disconnect VPN if connected if self.vpn_manager.is_connected(): self.vpn_manager.disconnect() if self.api_client: self.api_client.close() event.accept()