diff --git a/.gitignore b/.gitignore
index ea33d90..e147207 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1 @@
-internal/api/routes.gen.go
-internal/api/types.gen.go
-/repeater
\ No newline at end of file
+repeater
\ No newline at end of file
diff --git a/Makefile b/Makefile
deleted file mode 100644
index 2d57547..0000000
--- a/Makefile
+++ /dev/null
@@ -1,32 +0,0 @@
-.PHONY: build run clean install test
-
-BINARY_NAME=repeater
-CMD_PATH=./cmd/repeater
-BUILD_DIR=.
-
-build:
- go build -v -o $(BUILD_DIR)/$(BINARY_NAME) $(CMD_PATH)
-
-run: build
- sudo ./$(BINARY_NAME)
-
-clean:
- go clean
- rm -f $(BUILD_DIR)/$(BINARY_NAME)
-
-install: build
- sudo install -m 755 $(BUILD_DIR)/$(BINARY_NAME) /usr/local/bin/
-
-test:
- go test -v ./...
-
-tidy:
- go mod tidy
-
-fmt:
- go fmt ./...
-
-vet:
- go vet ./...
-
-all: fmt vet build
diff --git a/README.md b/README.md
deleted file mode 100644
index 54691f9..0000000
--- a/README.md
+++ /dev/null
@@ -1,102 +0,0 @@
-# Travel Router Control
-
-A Go application for controlling a mini travel router with dual WiFi interfaces and Ethernet connectivity. The router can operate as a WiFi repeater, connecting to upstream networks while providing a hotspot for client devices.
-
-## Features
-
-- WiFi network scanning and connection management
-- Hotspot (access point) configuration and control
-- Connected device monitoring
-- Real-time system logs via WebSocket
-- RESTful API following OpenAPI 3.0 specification
-- Web interface for easy management
-
-## Architecture
-
-The application follows a clean architecture pattern:
-
-```
-.
-├── cmd/
-│ └── repeater/ # Application entry point
-│ ├── main.go
-│ └── static/ # Embedded web assets
-├── internal/
-│ ├── api/ # HTTP API layer
-│ │ ├── router.go # Gin router setup
-│ │ └── handlers/ # HTTP handlers
-│ ├── app/ # Application logic & lifecycle
-│ ├── device/ # Device management
-│ ├── hotspot/ # Hotspot control
-│ ├── logging/ # Logging system
-│ ├── models/ # Data structures
-│ └── wifi/ # WiFi operations (wpa_supplicant via D-Bus)
-├── openapi.yaml # API specification
-└── go.mod
-```
-
-## Building
-
-```bash
-go build -o repeater ./cmd/repeater
-```
-
-## Running
-
-```bash
-sudo ./repeater
-```
-
-The application requires root privileges to:
-- Access D-Bus system bus for wpa_supplicant
-- Control systemd services (hostapd)
-- Read DHCP leases and ARP tables
-
-The server will start on port 8080.
-
-## API Endpoints
-
-### WiFi Operations
-- `GET /api/wifi/scan` - Scan for available networks
-- `POST /api/wifi/connect` - Connect to a network
-- `POST /api/wifi/disconnect` - Disconnect from current network
-
-### Hotspot Operations
-- `POST /api/hotspot/config` - Configure hotspot settings
-- `POST /api/hotspot/toggle` - Enable/disable hotspot
-
-### Device Management
-- `GET /api/devices` - Get connected devices
-
-### System
-- `GET /api/status` - Get system status
-- `GET /api/logs` - Get system logs
-- `DELETE /api/logs` - Clear logs
-
-### WebSocket
-- `GET /ws/logs` - Real-time log streaming
-
-See `openapi.yaml` for complete API documentation.
-
-## Configuration
-
-The application uses the following system resources:
-
-- **WiFi Interface**: `wlan0` (for upstream connection)
-- **AP Interface**: `wlan1` (for hotspot)
-- **Hostapd Config**: `/etc/hostapd/hostapd.conf`
-- **WPA Supplicant Config**: `/etc/wpa_supplicant/wpa_supplicant.conf`
-
-These can be modified in the respective package constants.
-
-## Dependencies
-
-- **Gin**: HTTP web framework
-- **godbus**: D-Bus client for wpa_supplicant control
-- **gorilla/websocket**: WebSocket support
-- **wpa_supplicant**: WiFi connection management
-- **hostapd**: Hotspot functionality
-
-## License
-
-MIT
diff --git a/cmd/repeater/main.go b/cmd/repeater/main.go
deleted file mode 100644
index 3fd1e66..0000000
--- a/cmd/repeater/main.go
+++ /dev/null
@@ -1,47 +0,0 @@
-package main
-
-import (
- "embed"
- "log"
- "os"
- "os/signal"
- "syscall"
-
- "github.com/nemunaire/repeater/internal/app"
- "github.com/nemunaire/repeater/internal/config"
-)
-
-//go:embed all:static
-var assets embed.FS
-
-func main() {
- // Load and parse options
- cfg, err := config.ConsolidateConfig()
- if err != nil {
- log.Fatal(err)
- }
-
- // Create application instance
- application := app.New(assets)
-
- // Initialize the application
- if err := application.Initialize(cfg); err != nil {
- log.Fatalf("Failed to initialize application: %v", err)
- }
- defer application.Shutdown()
-
- // Handle graceful shutdown
- sigChan := make(chan os.Signal, 1)
- signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
- go func() {
- <-sigChan
- log.Println("Shutting down gracefully...")
- application.Shutdown()
- os.Exit(0)
- }()
-
- // Start the server
- if err := application.Run(cfg.Bind); err != nil {
- log.Fatalf("Failed to start server: %v", err)
- }
-}
diff --git a/cmd/repeater/static/app.js b/cmd/repeater/static/app.js
deleted file mode 100644
index a7c367d..0000000
--- a/cmd/repeater/static/app.js
+++ /dev/null
@@ -1,791 +0,0 @@
-// Application state
-const appState = {
- selectedWifi: null,
- hotspotEnabled: true,
- autoScrollLogs: true,
- ws: null,
- wifiWs: null,
- reconnectAttempts: 0,
- wifiReconnectAttempts: 0,
- maxReconnectAttempts: 5,
- connectedSSID: null,
- connectionState: 'disconnected',
- networks: [],
- uptime: 0,
- uptimeInterval: null
-};
-
-// Initialize the application
-document.addEventListener('DOMContentLoaded', function() {
- initializeApp();
-});
-
-async function initializeApp() {
- console.log('Initializing Travel Router Control Panel...');
-
- // Load initial data
- await Promise.all([
- loadStatus(),
- scanWifi(),
- loadDevices(),
- loadLogs()
- ]);
-
- // Set up WebSockets for real-time updates
- connectWebSocket();
- connectWifiWebSocket();
-
- // Start periodic updates
- startPeriodicUpdates();
-
- // Start uptime counter
- startUptimeCounter();
-}
-
-// ===== API Functions =====
-
-async function loadStatus() {
- try {
- const response = await fetch('/api/status');
- const status = await response.json();
-
- updateStatusDisplay(status);
- return status;
- } catch (error) {
- console.error('Error loading status:', error);
- showToast('error', 'Erreur', 'Impossible de charger le statut');
- }
-}
-
-async function scanWifi() {
- const wifiList = document.getElementById('wifiList');
- const scanBtn = document.querySelector('[onclick="scanWifi()"]');
-
- if (scanBtn) {
- scanBtn.disabled = true;
- }
-
- wifiList.innerHTML = '
Recherche des réseaux disponibles...
';
-
- try {
- const response = await fetch('/api/wifi/scan');
- const networks = await response.json();
-
- appState.networks = networks;
- displayWifiNetworks(networks);
- showToast('success', 'Scan WiFi', `${networks.length} réseau(x) trouvé(s)`);
- } catch (error) {
- console.error('Error scanning WiFi:', error);
-
- // Fallback to cached networks
- try {
- const fallbackResponse = await fetch('/api/wifi/networks');
- const cachedNetworks = await fallbackResponse.json();
-
- appState.networks = cachedNetworks;
- displayWifiNetworks(cachedNetworks);
- showToast('warning', 'Scan échoué', `Affichage des réseaux en cache (${cachedNetworks.length})`);
- } catch (fallbackError) {
- console.error('Error loading cached networks:', fallbackError);
- wifiList.innerHTML = 'Erreur lors du scan
';
- showToast('error', 'Erreur', 'Échec du scan WiFi');
- }
- } finally {
- if (scanBtn) {
- scanBtn.disabled = false;
- }
- }
-}
-
-async function connectToWifi() {
- if (!appState.selectedWifi) {
- showToast('warning', 'Attention', 'Veuillez sélectionner un réseau WiFi');
- return;
- }
-
- const password = document.getElementById('wifiPassword').value;
-
- // Password requirement disabled
- // if (appState.selectedWifi.security !== 'Open' && !password) {
- // showToast('warning', 'Attention', 'Mot de passe requis pour ce réseau');
- // return;
- // }
-
- showLoading(true);
-
- try {
- const response = await fetch('/api/wifi/connect', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- ssid: appState.selectedWifi.ssid,
- password: password
- })
- });
-
- const result = await response.json();
-
- if (response.ok) {
- showToast('success', 'Connecté', `Connexion établie avec ${appState.selectedWifi.ssid}`);
- document.getElementById('wifiPassword').value = '';
- await loadStatus();
- } else {
- throw new Error(result.error || 'Connection failed');
- }
- } catch (error) {
- console.error('Error connecting to WiFi:', error);
- showToast('error', 'Erreur', 'Échec de la connexion: ' + error.message);
- } finally {
- showLoading(false);
- }
-}
-
-async function disconnectWifi() {
- showLoading(true);
-
- try {
- const response = await fetch('/api/wifi/disconnect', {
- method: 'POST'
- });
-
- if (response.ok) {
- showToast('success', 'Déconnecté', 'Déconnexion WiFi réussie');
- await loadStatus();
- // Force refresh WiFi list to remove green highlighting
- if (appState.networks.length > 0) {
- displayWifiNetworks(appState.networks);
- }
- } else {
- throw new Error('Disconnection failed');
- }
- } catch (error) {
- console.error('Error disconnecting WiFi:', error);
- showToast('error', 'Erreur', 'Échec de la déconnexion');
- } finally {
- showLoading(false);
- }
-}
-
-async function toggleHotspot() {
- const toggle = document.getElementById('hotspotToggle');
- const enabled = toggle.checked;
-
- showLoading(true);
-
- try {
- const response = await fetch('/api/hotspot/toggle', {
- method: 'POST'
- });
-
- const result = await response.json();
-
- if (response.ok) {
- appState.hotspotEnabled = result.enabled;
- toggle.checked = result.enabled;
- showToast('success', 'Hotspot', result.enabled ? 'Hotspot activé' : 'Hotspot désactivé');
- await loadStatus();
- } else {
- toggle.checked = !enabled;
- throw new Error('Toggle failed');
- }
- } catch (error) {
- console.error('Error toggling hotspot:', error);
- toggle.checked = !enabled;
- showToast('error', 'Erreur', 'Échec du basculement');
- } finally {
- showLoading(false);
- }
-}
-
-async function loadDevices() {
- try {
- const response = await fetch('/api/devices');
- const devices = await response.json();
-
- displayDevices(devices);
- document.getElementById('deviceCount').textContent = devices.length;
- } catch (error) {
- console.error('Error loading devices:', error);
- const devicesList = document.getElementById('devicesList');
- devicesList.innerHTML = '';
- }
-}
-
-async function loadLogs() {
- try {
- const response = await fetch('/api/logs');
- const logs = await response.json();
-
- const logContainer = document.getElementById('logContainer');
- logContainer.innerHTML = '';
-
- if (logs.length === 0) {
- logContainer.innerHTML = '';
- } else {
- logs.forEach(log => addLogEntry(log));
- }
- } catch (error) {
- console.error('Error loading logs:', error);
- }
-}
-
-async function clearLogs() {
- try {
- const response = await fetch('/api/logs', { method: 'DELETE' });
-
- if (response.ok) {
- const logContainer = document.getElementById('logContainer');
- logContainer.innerHTML = '';
- showToast('success', 'Logs', 'Logs effacés');
- }
- } catch (error) {
- console.error('Error clearing logs:', error);
- showToast('error', 'Erreur', 'Échec de la suppression des logs');
- }
-}
-
-// ===== Display Functions =====
-
-function updateStatusDisplay(status) {
- // Update WiFi status badge
- const wifiStatus = document.getElementById('wifiStatus');
- const wifiDot = wifiStatus.querySelector('.status-dot');
- const wifiText = wifiStatus.querySelector('.status-text');
-
- // Use connectionState for more detailed status
- const state = status.connectionState || (status.connected ? 'connected' : 'disconnected');
- appState.connectionState = state;
-
- switch (state) {
- case 'connected':
- wifiDot.className = 'status-dot active';
- wifiText.textContent = `Connecté: ${status.connectedSSID}`;
- break;
- case 'connecting':
- wifiDot.className = 'status-dot connecting';
- wifiText.textContent = status.connectedSSID ? `Connexion à ${status.connectedSSID}...` : 'Connexion...';
- break;
- case 'disconnecting':
- wifiDot.className = 'status-dot disconnecting';
- wifiText.textContent = 'Déconnexion...';
- break;
- case 'roaming':
- wifiDot.className = 'status-dot active';
- wifiText.textContent = `Roaming: ${status.connectedSSID}`;
- break;
- case 'disconnected':
- default:
- wifiDot.className = 'status-dot offline';
- wifiText.textContent = 'Déconnecté';
- break;
- }
-
- // Update hotspot status badge
- const hotspotStatus = document.getElementById('hotspotStatus');
- const hotspotDot = hotspotStatus.querySelector('.status-dot');
- const hotspotText = hotspotStatus.querySelector('.status-text');
- const hotspotToggle = document.getElementById('hotspotToggle');
-
- const isHotspotEnabled = status.hotspotStatus && status.hotspotStatus.state === 'ENABLED';
-
- if (isHotspotEnabled) {
- hotspotDot.className = 'status-dot active';
- const numStations = status.hotspotStatus.numStations || 0;
- hotspotText.textContent = `Hotspot actif (${numStations} client${numStations > 1 ? 's' : ''})`;
- hotspotToggle.checked = true;
- } else {
- hotspotDot.className = 'status-dot offline';
- hotspotText.textContent = 'Hotspot inactif';
- hotspotToggle.checked = false;
- }
-
- appState.hotspotEnabled = isHotspotEnabled;
-
- // Update hotspot details if available
- updateHotspotDetails(status.hotspotStatus);
-
- // Check if connectedSSID or state changed and refresh WiFi list if needed
- const prevSSID = appState.connectedSSID;
- const prevState = appState.connectionState;
- appState.connectedSSID = status.connectedSSID;
-
- const connectedChanged = prevSSID !== status.connectedSSID || prevState !== state;
-
- if (connectedChanged && appState.networks.length > 0) {
- displayWifiNetworks(appState.networks);
- }
-
- // Update stats
- document.getElementById('connectedDevices').textContent = status.connectedCount;
- document.getElementById('dataUsage').textContent = `${status.dataUsage.toFixed(1)} MB`;
-
- // Update uptime in state (will be incremented by the interval)
- appState.uptime = status.uptime;
- document.getElementById('uptime').textContent = formatUptime(status.uptime);
-
- document.getElementById('currentNetwork').textContent = status.connectedSSID || '-';
-}
-
-function displayWifiNetworks(networks) {
- const wifiList = document.getElementById('wifiList');
- wifiList.innerHTML = '';
-
- if (networks.length === 0) {
- wifiList.innerHTML = 'Aucun réseau trouvé
';
- return;
- }
-
- networks.forEach(network => {
- const wifiItem = document.createElement('div');
- wifiItem.className = 'wifi-item';
-
- // Mark the network based on connection state
- if (appState.connectedSSID && network.ssid === appState.connectedSSID) {
- switch (appState.connectionState) {
- case 'connected':
- case 'roaming':
- wifiItem.classList.add('connected');
- break;
- case 'connecting':
- wifiItem.classList.add('connecting');
- break;
- case 'disconnecting':
- wifiItem.classList.add('disconnecting');
- break;
- }
- }
-
- wifiItem.onclick = () => selectWifi(network, wifiItem);
-
- wifiItem.innerHTML = `
-
-
${escapeHtml(network.ssid)}
-
- ${escapeHtml(network.security)}
- Canal ${escapeHtml(String(network.channel))}
- ${escapeHtml(network.bssid)}
-
-
-
- ${generateSignalBars(network.signal)}
-
- `;
-
- wifiList.appendChild(wifiItem);
- });
-}
-
-function selectWifi(network, element) {
- document.querySelectorAll('.wifi-item').forEach(item => item.classList.remove('selected'));
- element.classList.add('selected');
- appState.selectedWifi = network;
-}
-
-function displayDevices(devices) {
- const devicesList = document.getElementById('devicesList');
- devicesList.innerHTML = '';
-
- if (devices.length === 0) {
- devicesList.innerHTML = `
-
-
-
Aucun appareil connecté
-
- `;
- return;
- }
-
- devices.forEach(device => {
- const deviceCard = document.createElement('div');
- deviceCard.className = 'device-card';
-
- deviceCard.innerHTML = `
- ${getDeviceIcon(device.type)}
- ${escapeHtml(device.name)}
- ${device.type}
-
-
${device.ip}
-
${device.mac}
-
- `;
-
- devicesList.appendChild(deviceCard);
- });
-}
-
-function addLogEntry(log) {
- const logContainer = document.getElementById('logContainer');
-
- // Remove placeholder if it exists
- const placeholder = logContainer.querySelector('.log-placeholder');
- if (placeholder) {
- placeholder.remove();
- }
-
- const logEntry = document.createElement('div');
- logEntry.className = 'log-entry';
-
- const timestamp = new Date(log.timestamp).toLocaleTimeString('fr-FR');
-
- const timestampSpan = document.createElement('span');
- timestampSpan.className = 'log-timestamp';
- timestampSpan.textContent = timestamp;
-
- const sourceSpan = document.createElement('span');
- sourceSpan.className = 'log-source';
- sourceSpan.textContent = `[${log.source}]`;
-
- const messageSpan = document.createElement('span');
- messageSpan.className = 'log-message';
- messageSpan.textContent = log.message;
-
- logEntry.appendChild(timestampSpan);
- logEntry.appendChild(sourceSpan);
- logEntry.appendChild(messageSpan);
-
- logContainer.appendChild(logEntry);
-
- if (appState.autoScrollLogs) {
- logContainer.scrollTop = logContainer.scrollHeight;
- }
-}
-
-function createHotspotInfoItem(label, value) {
- const item = document.createElement('div');
- item.className = 'hotspot-info-item';
-
- const labelSpan = document.createElement('span');
- labelSpan.className = 'info-label';
- labelSpan.textContent = label + ':';
-
- const valueSpan = document.createElement('span');
- valueSpan.className = 'info-value';
- valueSpan.textContent = value;
-
- item.appendChild(labelSpan);
- item.appendChild(valueSpan);
-
- return item;
-}
-
-function updateHotspotDetails(hotspotStatus) {
- const detailsContainer = document.getElementById('hotspotDetails');
- if (!detailsContainer) return;
-
- // Clear existing content
- detailsContainer.innerHTML = '';
-
- if (!hotspotStatus || hotspotStatus.state !== 'ENABLED') {
- detailsContainer.appendChild(createHotspotInfoItem('État', 'Inactif'));
- return;
- }
-
- detailsContainer.appendChild(createHotspotInfoItem('État', hotspotStatus.state));
-
- if (hotspotStatus.ssid) {
- detailsContainer.appendChild(createHotspotInfoItem('SSID', hotspotStatus.ssid));
- }
-
- if (hotspotStatus.channel) {
- detailsContainer.appendChild(createHotspotInfoItem('Canal', `${hotspotStatus.channel} (${hotspotStatus.frequency} MHz)`));
- }
-
- if (hotspotStatus.numStations !== undefined) {
- detailsContainer.appendChild(createHotspotInfoItem('Clients connectés', hotspotStatus.numStations.toString()));
- }
-
- if (hotspotStatus.bssid) {
- detailsContainer.appendChild(createHotspotInfoItem('BSSID', hotspotStatus.bssid));
- }
-}
-
-// ===== WebSocket Functions =====
-
-function connectWebSocket() {
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
- const wsUrl = `${protocol}//${window.location.host}/ws/logs`;
-
- try {
- appState.ws = new WebSocket(wsUrl);
-
- appState.ws.onopen = function() {
- console.log('WebSocket connected');
- appState.reconnectAttempts = 0;
- };
-
- appState.ws.onmessage = function(event) {
- try {
- const log = JSON.parse(event.data);
- addLogEntry(log);
- } catch (error) {
- console.error('Error parsing WebSocket message:', error);
- }
- };
-
- appState.ws.onerror = function(error) {
- console.error('WebSocket error:', error);
- };
-
- appState.ws.onclose = function() {
- console.log('WebSocket disconnected');
-
- // Attempt to reconnect
- if (appState.reconnectAttempts < appState.maxReconnectAttempts) {
- appState.reconnectAttempts++;
- setTimeout(connectWebSocket, 5000);
- }
- };
- } catch (error) {
- console.error('Error creating WebSocket:', error);
- }
-}
-
-function connectWifiWebSocket() {
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
- const wsUrl = `${protocol}//${window.location.host}/ws/wifi`;
-
- try {
- appState.wifiWs = new WebSocket(wsUrl);
-
- appState.wifiWs.onopen = function() {
- console.log('WiFi WebSocket connected');
- appState.wifiReconnectAttempts = 0;
- };
-
- appState.wifiWs.onmessage = function(event) {
- try {
- const msg = JSON.parse(event.data);
- handleWifiEvent(msg);
- } catch (error) {
- console.error('Error parsing WiFi WebSocket message:', error);
- }
- };
-
- appState.wifiWs.onerror = function(error) {
- console.error('WiFi WebSocket error:', error);
- };
-
- appState.wifiWs.onclose = function() {
- console.log('WiFi WebSocket disconnected');
-
- // Attempt to reconnect
- if (appState.wifiReconnectAttempts < appState.maxReconnectAttempts) {
- appState.wifiReconnectAttempts++;
- setTimeout(connectWifiWebSocket, 5000);
- }
- };
- } catch (error) {
- console.error('Error creating WiFi WebSocket:', error);
- }
-}
-
-function handleWifiEvent(event) {
- console.log('WiFi event received:', event.type, event);
-
- switch (event.type) {
- case 'scan_update':
- handleScanUpdate(event.data);
- break;
- case 'state_change':
- handleStateChange(event.data);
- break;
- case 'signal_update':
- handleSignalUpdate(event.data);
- break;
- default:
- console.warn('Unknown WiFi event type:', event.type);
- }
-}
-
-function handleScanUpdate(data) {
- // Update the network list in real-time
- appState.networks = data.networks;
- displayWifiNetworks(data.networks);
- console.log(`Scan update: ${data.networks.length} network(s) found`);
-}
-
-function handleStateChange(data) {
- // Update WiFi status badge
- const wifiStatus = document.getElementById('wifiStatus');
- const wifiDot = wifiStatus.querySelector('.status-dot');
- const wifiText = wifiStatus.querySelector('.status-text');
-
- // Update state in appState
- appState.connectionState = data.state;
-
- switch (data.state) {
- case 'connected':
- wifiDot.className = 'status-dot active';
- wifiText.textContent = `Connecté: ${data.ssid}`;
- appState.connectedSSID = data.ssid;
-
- // Refresh network list to show connected network
- if (appState.networks.length > 0) {
- displayWifiNetworks(appState.networks);
- }
- break;
-
- case 'connecting':
- wifiDot.className = 'status-dot connecting';
- wifiText.textContent = data.ssid ? `Connexion à ${data.ssid}...` : 'Connexion...';
-
- // Refresh network list to show connecting state
- if (appState.networks.length > 0) {
- displayWifiNetworks(appState.networks);
- }
- break;
-
- case 'disconnecting':
- wifiDot.className = 'status-dot disconnecting';
- wifiText.textContent = 'Déconnexion...';
-
- // Refresh network list to show disconnecting state
- if (appState.networks.length > 0) {
- displayWifiNetworks(appState.networks);
- }
- break;
-
- case 'roaming':
- wifiDot.className = 'status-dot active';
- wifiText.textContent = `Roaming: ${data.ssid}`;
- appState.connectedSSID = data.ssid;
- break;
-
- case 'disconnected':
- wifiDot.className = 'status-dot offline';
- wifiText.textContent = 'Déconnecté';
- appState.connectedSSID = null;
-
- // Refresh network list to remove connected highlighting
- if (appState.networks.length > 0) {
- displayWifiNetworks(appState.networks);
- }
- break;
-
- default:
- console.warn('Unknown WiFi state:', data.state);
- break;
- }
-
- console.log(`WiFi state changed: ${data.previous_state} → ${data.state}`, data.ssid);
-}
-
-function handleSignalUpdate(data) {
- // Update signal strength display if needed
- console.log(`Signal update for ${data.ssid}: ${data.signal}/5 (${data.dbm} dBm)`);
- // Could update the network list to reflect new signal strength
-}
-
-// ===== Utility Functions =====
-
-function generateSignalBars(strength) {
- const bars = [];
- for (let i = 1; i <= 4; i++) {
- const active = i <= strength ? 'active' : '';
- bars.push(``);
- }
- return `${bars.join('')}
`;
-}
-
-function getDeviceIcon(type) {
- const icons = {
- mobile: ``,
- laptop: ``,
- tablet: ``,
- desktop: ``,
- unknown: ``
- };
- return icons[type] || icons.unknown;
-}
-
-function formatUptime(seconds) {
- const hours = Math.floor(seconds / 3600);
- const minutes = Math.floor((seconds % 3600) / 60);
- const secs = seconds % 60;
-
- return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
-}
-
-function escapeHtml(text) {
- const div = document.createElement('div');
- div.textContent = text;
- return div.innerHTML;
-}
-
-function showToast(type, title, message) {
- const toast = document.getElementById('toast');
- const toastIcon = document.getElementById('toastIcon');
- const toastTitle = document.getElementById('toastTitle');
- const toastMessage = document.getElementById('toastMessage');
-
- const icons = {
- success: '',
- error: '',
- warning: '',
- info: ''
- };
-
- toast.className = `toast ${type}`;
- toastIcon.innerHTML = icons[type];
- toastTitle.textContent = title;
- toastMessage.textContent = message;
-
- setTimeout(() => toast.classList.add('show'), 10);
-
- setTimeout(() => toast.classList.remove('show'), 5000);
-}
-
-function hideToast() {
- document.getElementById('toast').classList.remove('show');
-}
-
-function showLoading(show) {
- const overlay = document.getElementById('loadingOverlay');
- if (show) {
- overlay.classList.add('show');
- } else {
- overlay.classList.remove('show');
- }
-}
-
-function toggleAutoScroll() {
- appState.autoScrollLogs = !appState.autoScrollLogs;
- const btn = document.getElementById('autoScrollBtn');
-
- if (appState.autoScrollLogs) {
- btn.style.opacity = '1';
- showToast('info', 'Auto-scroll', 'Auto-scroll activé');
- } else {
- btn.style.opacity = '0.5';
- showToast('info', 'Auto-scroll', 'Auto-scroll désactivé');
- }
-}
-
-// ===== Periodic Updates =====
-
-function startPeriodicUpdates() {
- // Update status every 5 seconds
- setInterval(() => {
- loadStatus();
- }, 5000);
-
- // Update devices every 10 seconds
- setInterval(() => {
- loadDevices();
- }, 10000);
-}
-
-function startUptimeCounter() {
- // Clear any existing interval
- if (appState.uptimeInterval) {
- clearInterval(appState.uptimeInterval);
- }
-
- // Increment uptime every second
- appState.uptimeInterval = setInterval(() => {
- appState.uptime++;
- document.getElementById('uptime').textContent = formatUptime(appState.uptime);
- }, 1000);
-}
diff --git a/cmd/repeater/static/index.html b/cmd/repeater/static/index.html
deleted file mode 100644
index d53a645..0000000
--- a/cmd/repeater/static/index.html
+++ /dev/null
@@ -1,231 +0,0 @@
-
-
-
-
-
- Travel Router Control Panel
-
-
-
-
-
-
-
-
-
-
-
0
-
Appareils connectés
-
-
-
-
-
-
0 MB
-
Données utilisées
-
-
-
-
-
-
00:00:00
-
Temps de fonctionnement
-
-
-
-
-
-
-
-
-
-
-
-
-
- État:
- Chargement...
-
-
-
-
-
-
-
-
-
-
-
-
-
Aucun appareil connecté
-
-
-
-
-
-
-
-
-
-
-
Aucun log disponible
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/cmd/repeater/static/style.css b/cmd/repeater/static/style.css
deleted file mode 100644
index b518696..0000000
--- a/cmd/repeater/static/style.css
+++ /dev/null
@@ -1,878 +0,0 @@
-/* CSS Variables for theming */
-:root {
- --primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- --secondary-gradient: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
- --success-color: #10b981;
- --danger-color: #ef4444;
- --warning-color: #f59e0b;
- --info-color: #3b82f6;
- --background: #f3f4f6;
- --card-background: #ffffff;
- --text-primary: #1f2937;
- --text-secondary: #6b7280;
- --border-color: #e5e7eb;
- --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
- --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
- --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
- --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
- --radius-sm: 0.375rem;
- --radius-md: 0.5rem;
- --radius-lg: 0.75rem;
- --radius-xl: 1rem;
-}
-
-* {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
-}
-
-body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
- background: var(--primary-gradient);
- min-height: 100vh;
- padding: 1.5rem;
- color: var(--text-primary);
-}
-
-.container {
- max-width: 1400px;
- margin: 0 auto;
- background: rgba(255, 255, 255, 0.98);
- backdrop-filter: blur(20px);
- border-radius: var(--radius-xl);
- padding: 2rem;
- box-shadow: var(--shadow-xl);
-}
-
-/* Header Styles */
-.header {
- margin-bottom: 2rem;
- padding-bottom: 1.5rem;
- border-bottom: 2px solid var(--border-color);
-}
-
-.header-content {
- display: flex;
- justify-content: space-between;
- align-items: center;
- flex-wrap: wrap;
- gap: 1rem;
-}
-
-.header h1 {
- color: var(--text-primary);
- font-size: 2rem;
- font-weight: 700;
- display: flex;
- align-items: center;
- gap: 0.75rem;
-}
-
-.logo-icon {
- color: #667eea;
-}
-
-.connection-info {
- display: flex;
- gap: 1rem;
- flex-wrap: wrap;
-}
-
-.status-badge {
- display: inline-flex;
- align-items: center;
- gap: 0.5rem;
- padding: 0.5rem 1rem;
- border-radius: 9999px;
- font-size: 0.875rem;
- font-weight: 500;
- background: var(--background);
- border: 1px solid var(--border-color);
-}
-
-.status-dot {
- width: 0.625rem;
- height: 0.625rem;
- border-radius: 50%;
- animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
-}
-
-.status-dot.active {
- background: var(--success-color);
-}
-
-.status-dot.offline {
- background: var(--text-secondary);
- animation: none;
-}
-
-.status-dot.connecting {
- background: var(--warning-color);
- animation: blink 1s ease-in-out infinite;
-}
-
-.status-dot.disconnecting {
- background: var(--warning-color);
- animation: blink 1s ease-in-out infinite;
-}
-
-@keyframes pulse {
- 0%, 100% { opacity: 1; }
- 50% { opacity: 0.5; }
-}
-
-@keyframes blink {
- 0%, 100% { opacity: 1; }
- 50% { opacity: 0.3; }
-}
-
-/* Stats Grid */
-.stats-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
- gap: 1.5rem;
- margin-bottom: 2rem;
-}
-
-.stat-card {
- background: var(--card-background);
- border-radius: var(--radius-lg);
- padding: 1.5rem;
- box-shadow: var(--shadow-md);
- border: 1px solid var(--border-color);
- display: flex;
- gap: 1rem;
- align-items: center;
- transition: all 0.3s ease;
- max-width: 100%;
-}
-
-.stat-card:hover {
- transform: translateY(-2px);
- box-shadow: var(--shadow-lg);
-}
-
-.stat-icon {
- width: 3rem;
- height: 3rem;
- border-radius: var(--radius-md);
- background: var(--primary-gradient);
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
-}
-
-.stat-icon svg {
- color: white;
-}
-
-.stat-content {
- flex: 1;
- min-width: 0;
-}
-
-.stat-value {
- font-size: 1.875rem;
- font-weight: 700;
- color: var(--text-primary);
- line-height: 1;
- margin-bottom: 0.25rem;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.stat-label {
- font-size: 0.875rem;
- color: var(--text-secondary);
- font-weight: 500;
-}
-
-/* Grid Layout */
-.grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
- gap: 1.5rem;
- margin-bottom: 1.5rem;
-}
-
-/* Card Styles */
-.card {
- background: var(--card-background);
- border-radius: var(--radius-lg);
- padding: 1.5rem;
- box-shadow: var(--shadow-md);
- border: 1px solid var(--border-color);
- transition: all 0.3s ease;
-}
-
-.card:hover {
- box-shadow: var(--shadow-lg);
-}
-
-.card-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 1.5rem;
- padding-bottom: 1rem;
- border-bottom: 1px solid var(--border-color);
-}
-
-.card-header h2 {
- color: var(--text-primary);
- font-size: 1.25rem;
- font-weight: 600;
- display: flex;
- align-items: center;
- gap: 0.5rem;
- margin: 0;
-}
-
-.icon {
- color: #667eea;
-}
-
-/* Form Elements */
-.form-group {
- margin-bottom: 1.25rem;
-}
-
-.form-group label {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- margin-bottom: 0.5rem;
- color: var(--text-primary);
- font-weight: 500;
- font-size: 0.875rem;
-}
-
-.input-icon {
- color: var(--text-secondary);
-}
-
-.form-group input,
-.form-group select {
- width: 100%;
- padding: 0.75rem 1rem;
- border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- font-size: 0.9375rem;
- transition: all 0.2s ease;
- background: var(--card-background);
- color: var(--text-primary);
-}
-
-.form-group input:focus,
-.form-group select:focus {
- outline: none;
- border-color: #667eea;
- box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
-}
-
-/* Buttons */
-.btn {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 0.5rem;
- padding: 0.75rem 1.5rem;
- border: none;
- border-radius: var(--radius-md);
- font-size: 0.9375rem;
- font-weight: 500;
- cursor: pointer;
- transition: all 0.2s ease;
- width: 100%;
- color: white;
-}
-
-.btn svg {
- flex-shrink: 0;
-}
-
-.btn-primary {
- background: var(--primary-gradient);
-}
-
-.btn-primary:hover {
- transform: translateY(-1px);
- box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
-}
-
-.btn-secondary {
- background: var(--text-secondary);
-}
-
-.btn-secondary:hover {
- background: #4b5563;
- transform: translateY(-1px);
-}
-
-.btn-danger {
- background: var(--danger-color);
-}
-
-.btn-danger:hover {
- background: #dc2626;
- transform: translateY(-1px);
-}
-
-.btn:active {
- transform: translateY(0);
-}
-
-.btn:disabled {
- opacity: 0.5;
- cursor: not-allowed;
-}
-
-.btn-icon {
- background: transparent;
- border: none;
- padding: 0.5rem;
- border-radius: var(--radius-sm);
- cursor: pointer;
- color: var(--text-secondary);
- transition: all 0.2s ease;
- display: inline-flex;
- align-items: center;
- justify-content: center;
-}
-
-.btn-icon:hover {
- background: var(--background);
- color: var(--text-primary);
-}
-
-.button-group {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 0.75rem;
-}
-
-/* Toggle Switch */
-.toggle-switch {
- position: relative;
- display: inline-block;
- width: 50px;
- height: 26px;
-}
-
-.toggle-switch input {
- opacity: 0;
- width: 0;
- height: 0;
-}
-
-.toggle-switch label {
- position: absolute;
- cursor: pointer;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background-color: var(--text-secondary);
- transition: 0.3s;
- border-radius: 34px;
-}
-
-.toggle-switch label:before {
- position: absolute;
- content: "";
- height: 20px;
- width: 20px;
- left: 3px;
- bottom: 3px;
- background-color: white;
- transition: 0.3s;
- border-radius: 50%;
-}
-
-.toggle-switch input:checked + label {
- background: var(--success-color);
-}
-
-.toggle-switch input:checked + label:before {
- transform: translateX(24px);
-}
-
-/* WiFi List */
-.wifi-list {
- max-height: 320px;
- overflow-y: auto;
- border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- background: var(--background);
-}
-
-.wifi-item {
- padding: 1rem;
- border-bottom: 1px solid var(--border-color);
- cursor: pointer;
- transition: all 0.2s ease;
- display: flex;
- justify-content: space-between;
- align-items: center;
- background: var(--card-background);
-}
-
-.wifi-item:hover {
- background: var(--background);
-}
-
-.wifi-item:last-child {
- border-bottom: none;
-}
-
-.wifi-item.selected {
- background: #eff6ff;
- border-left: 4px solid #667eea;
-}
-
-.wifi-item.connected {
- background: #d1fae5;
- border-left-color: var(--success-color) !important;
-}
-
-.wifi-item.connected .wifi-ssid {
- color: var(--success-color);
- font-weight: 700;
-}
-
-.wifi-item.connecting {
- background: #fef3c7;
- border-left-color: var(--warning-color) !important;
- animation: pulse-item 1.5s ease-in-out infinite;
-}
-
-.wifi-item.connecting .wifi-ssid {
- color: var(--warning-color);
- font-weight: 700;
-}
-
-.wifi-item.disconnecting {
- background: #fee2e2;
- border-left-color: #dc2626 !important;
- animation: pulse-item 1.5s ease-in-out infinite;
-}
-
-.wifi-item.disconnecting .wifi-ssid {
- color: #dc2626;
- font-weight: 700;
-}
-
-@keyframes pulse-item {
- 0%, 100% { opacity: 1; }
- 50% { opacity: 0.7; }
-}
-
-.wifi-item.loading {
- justify-content: center;
- color: var(--text-secondary);
- cursor: default;
-}
-
-.wifi-info {
- flex: 1;
-}
-
-.wifi-ssid {
- font-weight: 600;
- color: var(--text-primary);
- margin-bottom: 0.25rem;
-}
-
-.wifi-details {
- font-size: 0.8125rem;
- color: var(--text-secondary);
- display: flex;
- gap: 1rem;
-}
-
-.wifi-signal {
- display: flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.signal-bars {
- display: flex;
- gap: 2px;
- align-items: flex-end;
- height: 16px;
-}
-
-.signal-bar {
- width: 3px;
- background: var(--border-color);
- border-radius: 2px;
-}
-
-.signal-bar:nth-child(1) { height: 25%; }
-.signal-bar:nth-child(2) { height: 50%; }
-.signal-bar:nth-child(3) { height: 75%; }
-.signal-bar:nth-child(4) { height: 100%; }
-
-.signal-bar.active {
- background: var(--success-color);
-}
-
-/* Devices */
-.devices-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
- gap: 1rem;
- min-height: 200px;
-}
-
-.device-card {
- background: var(--background);
- border-radius: var(--radius-md);
- padding: 1.25rem;
- text-align: center;
- border: 1px solid var(--border-color);
- transition: all 0.2s ease;
-}
-
-.device-card:hover {
- transform: translateY(-2px);
- box-shadow: var(--shadow-md);
-}
-
-.device-icon {
- width: 48px;
- height: 48px;
- margin: 0 auto 0.75rem;
- color: #667eea;
-}
-
-.device-name {
- font-weight: 600;
- color: var(--text-primary);
- margin-bottom: 0.25rem;
- font-size: 0.9375rem;
-}
-
-.device-type {
- font-size: 0.75rem;
- color: var(--text-secondary);
- text-transform: uppercase;
- letter-spacing: 0.05em;
- margin-bottom: 0.5rem;
-}
-
-.device-info {
- font-size: 0.8125rem;
- color: var(--text-secondary);
-}
-
-.device-placeholder {
- grid-column: 1 / -1;
- text-align: center;
- padding: 3rem 1rem;
- color: var(--text-secondary);
-}
-
-.device-placeholder svg {
- margin-bottom: 1rem;
-}
-
-.device-count {
- background: var(--primary-gradient);
- color: white;
- padding: 0.25rem 0.75rem;
- border-radius: 9999px;
- font-size: 0.875rem;
- font-weight: 600;
-}
-
-/* Logs */
-.log-container {
- background: #1f2937;
- color: #10b981;
- padding: 1rem;
- border-radius: var(--radius-md);
- font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Mono', 'Consolas', 'Courier New', monospace;
- font-size: 0.8125rem;
- max-height: 400px;
- overflow-y: auto;
- line-height: 1.6;
- min-height: 200px;
-}
-
-.log-entry {
- margin-bottom: 0.5rem;
- display: flex;
- gap: 0.75rem;
-}
-
-.log-timestamp {
- color: #6b7280;
- flex-shrink: 0;
-}
-
-.log-source {
- color: #3b82f6;
- font-weight: 600;
- flex-shrink: 0;
- min-width: 80px;
-}
-
-.log-message {
- color: #d1d5db;
-}
-
-.log-controls {
- display: flex;
- gap: 0.5rem;
-}
-
-.log-placeholder {
- text-align: center;
- padding: 3rem 1rem;
- color: #6b7280;
-}
-
-.log-placeholder svg {
- margin-bottom: 1rem;
-}
-
-/* Toast Notifications */
-.toast {
- position: fixed;
- top: 1.5rem;
- right: 1.5rem;
- max-width: 400px;
- background: white;
- border-radius: var(--radius-lg);
- box-shadow: var(--shadow-xl);
- padding: 1rem;
- display: flex;
- gap: 0.75rem;
- align-items: start;
- transform: translateX(calc(100% + 2rem));
- transition: transform 0.3s ease;
- z-index: 1000;
- border: 1px solid var(--border-color);
-}
-
-.toast.show {
- transform: translateX(0);
-}
-
-.toast-icon {
- width: 40px;
- height: 40px;
- border-radius: var(--radius-md);
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
-}
-
-.toast.success .toast-icon {
- background: #d1fae5;
- color: var(--success-color);
-}
-
-.toast.error .toast-icon {
- background: #fee2e2;
- color: var(--danger-color);
-}
-
-.toast.warning .toast-icon {
- background: #fef3c7;
- color: var(--warning-color);
-}
-
-.toast.info .toast-icon {
- background: #dbeafe;
- color: var(--info-color);
-}
-
-.toast-content {
- flex: 1;
-}
-
-.toast-title {
- font-weight: 600;
- color: var(--text-primary);
- margin-bottom: 0.25rem;
-}
-
-.toast-message {
- font-size: 0.875rem;
- color: var(--text-secondary);
-}
-
-.toast-close {
- background: transparent;
- border: none;
- color: var(--text-secondary);
- cursor: pointer;
- padding: 0.25rem;
- border-radius: var(--radius-sm);
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.2s ease;
-}
-
-.toast-close:hover {
- background: var(--background);
- color: var(--text-primary);
-}
-
-/* Loading Overlay */
-.loading-overlay {
- position: fixed;
- inset: 0;
- background: rgba(0, 0, 0, 0.5);
- backdrop-filter: blur(4px);
- display: none;
- align-items: center;
- justify-content: center;
- z-index: 2000;
- flex-direction: column;
- gap: 1rem;
-}
-
-.loading-overlay.show {
- display: flex;
-}
-
-.loading-overlay p {
- color: white;
- font-weight: 500;
-}
-
-.spinner {
- width: 48px;
- height: 48px;
- border: 4px solid rgba(255, 255, 255, 0.3);
- border-top-color: white;
- border-radius: 50%;
- animation: spin 0.8s linear infinite;
-}
-
-@keyframes spin {
- to { transform: rotate(360deg); }
-}
-
-/* Scrollbar Styling */
-.wifi-list::-webkit-scrollbar,
-.log-container::-webkit-scrollbar {
- width: 8px;
-}
-
-.wifi-list::-webkit-scrollbar-track,
-.log-container::-webkit-scrollbar-track {
- background: transparent;
-}
-
-.wifi-list::-webkit-scrollbar-thumb {
- background: var(--border-color);
- border-radius: 4px;
-}
-
-.log-container::-webkit-scrollbar-thumb {
- background: #374151;
- border-radius: 4px;
-}
-
-.wifi-list::-webkit-scrollbar-thumb:hover,
-.log-container::-webkit-scrollbar-thumb:hover {
- background: var(--text-secondary);
-}
-
-/* Hotspot Details */
-.hotspot-details {
- margin-top: 1.5rem;
- padding: 1rem;
- background: var(--background);
- border-radius: var(--radius-md);
- border: 1px solid var(--border-color);
-}
-
-.hotspot-info-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 0.5rem 0;
- border-bottom: 1px solid var(--border-color);
-}
-
-.hotspot-info-item:last-child {
- border-bottom: none;
-}
-
-.info-label {
- font-weight: 500;
- color: var(--text-secondary);
- font-size: 0.875rem;
-}
-
-.info-value {
- font-weight: 600;
- color: var(--text-primary);
- font-size: 0.875rem;
-}
-
-/* Responsive Design */
-@media (max-width: 1024px) {
- .grid {
- grid-template-columns: 1fr;
- }
-}
-
-@media (max-width: 768px) {
- body {
- padding: 1rem;
- }
-
- .container {
- padding: 1.5rem;
- }
-
- .header h1 {
- font-size: 1.5rem;
- }
-
- .header-content {
- flex-direction: column;
- align-items: flex-start;
- }
-
- .stats-grid {
- grid-template-columns: 1fr;
- }
-
- .grid {
- grid-template-columns: 1fr;
- }
-
- .button-group {
- grid-template-columns: 1fr;
- }
-
- .toast {
- left: 1rem;
- right: 1rem;
- max-width: none;
- }
-}
-
-@media (max-width: 480px) {
- .stat-card {
- flex-direction: column;
- text-align: center;
- }
-
- .devices-grid {
- grid-template-columns: 1fr;
- }
-}
diff --git a/generate.go b/generate.go
deleted file mode 100644
index c01b94b..0000000
--- a/generate.go
+++ /dev/null
@@ -1,4 +0,0 @@
-package main
-
-//go:generate go tool oapi-codegen -config oapi-types.cfg.yaml openapi.yaml
-//go:generate go tool oapi-codegen -config oapi-gin.cfg.yaml openapi.yaml
diff --git a/go.mod b/go.mod
index 5e17b30..0f2fed9 100644
--- a/go.mod
+++ b/go.mod
@@ -1,62 +1,9 @@
-module github.com/nemunaire/repeater
+module git.nemunai.re/nemunaire/repeater
go 1.24.4
require (
- github.com/getkin/kin-openapi v0.132.0
- github.com/gin-gonic/gin v1.11.0
github.com/godbus/dbus/v5 v5.1.0
github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.3
)
-
-require (
- github.com/bytedance/sonic v1.14.0 // indirect
- github.com/bytedance/sonic/loader v0.3.0 // indirect
- github.com/cloudwego/base64x v0.1.6 // indirect
- github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect
- github.com/gabriel-vasile/mimetype v1.4.8 // indirect
- github.com/gin-contrib/sse v1.1.0 // indirect
- github.com/go-openapi/jsonpointer v0.21.0 // indirect
- github.com/go-openapi/swag v0.23.0 // indirect
- github.com/go-playground/locales v0.14.1 // indirect
- github.com/go-playground/universal-translator v0.18.1 // indirect
- github.com/go-playground/validator/v10 v10.27.0 // indirect
- github.com/goccy/go-json v0.10.2 // indirect
- github.com/goccy/go-yaml v1.18.0 // indirect
- github.com/josharian/intern v1.0.0 // indirect
- github.com/json-iterator/go v1.1.12 // indirect
- github.com/klauspost/cpuid/v2 v2.3.0 // indirect
- github.com/leodido/go-urn v1.4.0 // indirect
- github.com/mailru/easyjson v0.7.7 // indirect
- github.com/mattn/go-isatty v0.0.20 // indirect
- github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
- github.com/modern-go/reflect2 v1.0.2 // indirect
- github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
- github.com/oapi-codegen/oapi-codegen/v2 v2.5.0 // indirect
- github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
- github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
- github.com/pelletier/go-toml/v2 v2.2.4 // indirect
- github.com/perimeterx/marshmallow v1.1.5 // indirect
- github.com/quic-go/qpack v0.5.1 // indirect
- github.com/quic-go/quic-go v0.54.0 // indirect
- github.com/speakeasy-api/jsonpath v0.6.0 // indirect
- github.com/speakeasy-api/openapi-overlay v0.10.2 // indirect
- github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
- github.com/ugorji/go/codec v1.3.0 // indirect
- github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect
- go.uber.org/mock v0.5.0 // indirect
- golang.org/x/arch v0.20.0 // indirect
- golang.org/x/crypto v0.40.0 // indirect
- golang.org/x/mod v0.25.0 // indirect
- golang.org/x/net v0.42.0 // indirect
- golang.org/x/sync v0.16.0 // indirect
- golang.org/x/sys v0.35.0 // indirect
- golang.org/x/text v0.27.0 // indirect
- golang.org/x/tools v0.34.0 // indirect
- google.golang.org/protobuf v1.36.9 // indirect
- gopkg.in/yaml.v2 v2.4.0 // indirect
- gopkg.in/yaml.v3 v3.0.1 // indirect
-)
-
-tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen
diff --git a/go.sum b/go.sum
index 17956cd..0546936 100644
--- a/go.sum
+++ b/go.sum
@@ -1,239 +1,6 @@
-github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
-github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
-github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
-github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
-github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
-github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
-github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
-github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
-github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58=
-github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 h1:PRxIJD8XjimM5aTknUK9w6DHLDox2r2M3DI4i2pnd3w=
-github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5qlB+mlV1okblJqcSMtR4c52UKxDiX9GRBS8+Q=
-github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
-github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
-github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
-github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
-github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
-github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk=
-github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58=
-github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
-github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
-github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
-github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
-github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
-github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
-github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
-github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
-github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
-github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
-github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
-github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
-github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
-github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
-github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
-github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
-github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
-github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
-github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
-github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
-github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
-github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
-github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
-github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
-github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
-github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
-github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
-github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
-github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
-github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
-github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
-github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
-github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
-github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
-github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
-github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
-github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
-github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
-github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
-github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
-github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
-github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
-github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
-github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
-github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
-github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
-github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
-github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
-github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
-github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
-github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
-github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
-github.com/oapi-codegen/oapi-codegen/v2 v2.5.0 h1:iJvF8SdB/3/+eGOXEpsWkD8FQAHj6mqkb6Fnsoc8MFU=
-github.com/oapi-codegen/oapi-codegen/v2 v2.5.0/go.mod h1:fwlMxUEMuQK5ih9aymrxKPQqNm2n8bdLk1ppjH+lr9w=
-github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
-github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
-github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
-github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
-github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
-github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
-github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
-github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
-github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
-github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
-github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
-github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
-github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw=
-github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
-github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
-github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
-github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
-github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
-github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
-github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
-github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
-github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
-github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
-github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
-github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
-github.com/speakeasy-api/jsonpath v0.6.0 h1:IhtFOV9EbXplhyRqsVhHoBmmYjblIRh5D1/g8DHMXJ8=
-github.com/speakeasy-api/jsonpath v0.6.0/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw=
-github.com/speakeasy-api/openapi-overlay v0.10.2 h1:VOdQ03eGKeiHnpb1boZCGm7x8Haj6gST0P3SGTX95GU=
-github.com/speakeasy-api/openapi-overlay v0.10.2/go.mod h1:n0iOU7AqKpNFfEt6tq7qYITC4f0yzVVdFw0S7hukemg=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
-github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
-github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
-github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
-github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
-github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
-github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
-github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
-github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk=
-github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ=
-github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
-go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
-golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
-golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
-golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
-golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
-golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
-golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
-golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
-golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
-golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
-golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
-golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
-golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
-golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
-golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
-google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
-google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
-google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
-google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
-google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
-google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
-google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
-gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
-gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
-gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
-gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
-gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
-gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
-gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
-gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/internal/api/handlers/handlers.go b/internal/api/handlers/handlers.go
deleted file mode 100644
index 7c87a7c..0000000
--- a/internal/api/handlers/handlers.go
+++ /dev/null
@@ -1,148 +0,0 @@
-package handlers
-
-import (
- "net/http"
-
- "github.com/gin-gonic/gin"
- "github.com/nemunaire/repeater/internal/config"
- "github.com/nemunaire/repeater/internal/hotspot"
- "github.com/nemunaire/repeater/internal/logging"
- "github.com/nemunaire/repeater/internal/models"
- "github.com/nemunaire/repeater/internal/station"
- "github.com/nemunaire/repeater/internal/wifi"
-)
-
-// GetWiFiNetworks returns cached WiFi networks without scanning
-func GetWiFiNetworks(c *gin.Context) {
- networks, err := wifi.GetCachedNetworks()
- if err != nil {
- logging.AddLog("WiFi", "Erreur lors de la récupération des réseaux: "+err.Error())
- c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur lors de la récupération des réseaux"})
- return
- }
-
- c.JSON(http.StatusOK, networks)
-}
-
-// ScanWiFi handles WiFi network scanning
-func ScanWiFi(c *gin.Context) {
- networks, err := wifi.ScanNetworks()
- if err != nil {
- logging.AddLog("WiFi", "Erreur lors du scan: "+err.Error())
- c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur lors du scan WiFi"})
- return
- }
-
- logging.AddLog("WiFi", "Scan terminé - "+string(rune(len(networks)))+" réseaux trouvés")
- c.JSON(http.StatusOK, networks)
-}
-
-// ConnectWiFi handles WiFi connection requests
-func ConnectWiFi(c *gin.Context) {
- var req models.WiFiConnectRequest
- if err := c.ShouldBindJSON(&req); err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"})
- return
- }
-
- logging.AddLog("WiFi", "Tentative de connexion à "+req.SSID)
-
- err := wifi.Connect(req.SSID, req.Password)
- if err != nil {
- logging.AddLog("WiFi", "Échec de connexion: "+err.Error())
- c.JSON(http.StatusInternalServerError, gin.H{"error": "Échec de connexion: " + err.Error()})
- return
- }
-
- logging.AddLog("WiFi", "Connexion réussie à "+req.SSID)
- c.JSON(http.StatusOK, gin.H{"status": "success"})
-}
-
-// DisconnectWiFi handles WiFi disconnection
-func DisconnectWiFi(c *gin.Context) {
- logging.AddLog("WiFi", "Tentative de déconnexion")
-
- err := wifi.Disconnect()
- if err != nil {
- logging.AddLog("WiFi", "Erreur de déconnexion: "+err.Error())
- c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur de déconnexion: " + err.Error()})
- return
- }
-
- logging.AddLog("WiFi", "Déconnexion réussie")
- c.JSON(http.StatusOK, gin.H{"status": "success"})
-}
-
-// ConfigureHotspot handles hotspot configuration
-func ConfigureHotspot(c *gin.Context) {
- var config models.HotspotConfig
- if err := c.ShouldBindJSON(&config); err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"})
- return
- }
-
- err := hotspot.Configure(config)
- if err != nil {
- logging.AddLog("Hotspot", "Erreur de configuration: "+err.Error())
- c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur de configuration: " + err.Error()})
- return
- }
-
- logging.AddLog("Hotspot", "Configuration mise à jour: "+config.SSID)
- c.JSON(http.StatusOK, gin.H{"status": "success"})
-}
-
-// ToggleHotspot handles hotspot enable/disable
-func ToggleHotspot(c *gin.Context, status *models.SystemStatus) {
- // Determine current state
- isEnabled := status.HotspotStatus != nil && status.HotspotStatus.State == "ENABLED"
-
- var err error
- if !isEnabled {
- err = hotspot.Start()
- logging.AddLog("Hotspot", "Hotspot activé")
- } else {
- err = hotspot.Stop()
- logging.AddLog("Hotspot", "Hotspot désactivé")
- }
-
- if err != nil {
- logging.AddLog("Hotspot", "Erreur: "+err.Error())
- c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur: " + err.Error()})
- return
- }
-
- // Update status immediately
- status.HotspotStatus = hotspot.GetDetailedStatus()
-
- c.JSON(http.StatusOK, gin.H{"enabled": !isEnabled})
-}
-
-// GetDevices returns connected devices
-func GetDevices(c *gin.Context, cfg *config.Config) {
- devices, err := station.GetStations()
- if err != nil {
- logging.AddLog("Système", "Erreur lors de la récupération des appareils: "+err.Error())
- c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur lors de la récupération des appareils"})
- return
- }
-
- c.JSON(http.StatusOK, devices)
-}
-
-// GetStatus returns system status
-func GetStatus(c *gin.Context, status *models.SystemStatus) {
- c.JSON(http.StatusOK, status)
-}
-
-// GetLogs returns system logs
-func GetLogs(c *gin.Context) {
- logs := logging.GetLogs()
- c.JSON(http.StatusOK, logs)
-}
-
-// ClearLogs clears system logs
-func ClearLogs(c *gin.Context) {
- logging.ClearLogs()
- c.JSON(http.StatusOK, gin.H{"status": "success"})
-}
diff --git a/internal/api/handlers/websocket.go b/internal/api/handlers/websocket.go
deleted file mode 100644
index 87d2823..0000000
--- a/internal/api/handlers/websocket.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package handlers
-
-import (
- "log"
- "net/http"
-
- "github.com/gin-gonic/gin"
- "github.com/gorilla/websocket"
- "github.com/nemunaire/repeater/internal/logging"
-)
-
-var upgrader = websocket.Upgrader{
- CheckOrigin: func(r *http.Request) bool {
- return true
- },
-}
-
-// WebSocketLogs handles WebSocket connections for real-time logs
-func WebSocketLogs(c *gin.Context) {
- conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
- if err != nil {
- log.Printf("Erreur WebSocket: %v", err)
- return
- }
- defer conn.Close()
-
- // Register client
- logging.RegisterWebSocketClient(conn)
- defer logging.UnregisterWebSocketClient(conn)
-
- // Keep connection alive
- for {
- _, _, err := conn.ReadMessage()
- if err != nil {
- break
- }
- }
-}
diff --git a/internal/api/handlers/websocket_wifi.go b/internal/api/handlers/websocket_wifi.go
deleted file mode 100644
index ff8d132..0000000
--- a/internal/api/handlers/websocket_wifi.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package handlers
-
-import (
- "log"
-
- "github.com/gin-gonic/gin"
- "github.com/nemunaire/repeater/internal/wifi"
-)
-
-// WebSocketWifi handles WebSocket connections for real-time WiFi events
-func WebSocketWifi(c *gin.Context) {
- conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
- if err != nil {
- log.Printf("Erreur WebSocket WiFi: %v", err)
- return
- }
- defer conn.Close()
-
- // Register client
- wifi.RegisterWebSocketClient(conn)
- defer wifi.UnregisterWebSocketClient(conn)
-
- // Keep connection alive
- for {
- _, _, err := conn.ReadMessage()
- if err != nil {
- break
- }
- }
-}
diff --git a/internal/api/router.go b/internal/api/router.go
deleted file mode 100644
index 956661e..0000000
--- a/internal/api/router.go
+++ /dev/null
@@ -1,69 +0,0 @@
-package api
-
-import (
- "embed"
- "io/fs"
- "net/http"
-
- "github.com/gin-gonic/gin"
- "github.com/nemunaire/repeater/internal/api/handlers"
- "github.com/nemunaire/repeater/internal/config"
- "github.com/nemunaire/repeater/internal/models"
-)
-
-// SetupRouter creates and configures the Gin router
-func SetupRouter(status *models.SystemStatus, cfg *config.Config, assets embed.FS) *gin.Engine {
- // Set Gin to release mode (can be overridden with GIN_MODE env var)
- gin.SetMode(gin.ReleaseMode)
-
- r := gin.Default()
-
- // API routes
- api := r.Group("/api")
- {
- // WiFi endpoints
- wifi := api.Group("/wifi")
- {
- wifi.GET("/networks", handlers.GetWiFiNetworks)
- wifi.GET("/scan", handlers.ScanWiFi)
- wifi.POST("/connect", handlers.ConnectWiFi)
- wifi.POST("/disconnect", handlers.DisconnectWiFi)
- }
-
- // Hotspot endpoints
- hotspot := api.Group("/hotspot")
- {
- hotspot.POST("/config", handlers.ConfigureHotspot)
- hotspot.POST("/toggle", func(c *gin.Context) {
- handlers.ToggleHotspot(c, status)
- })
- }
-
- // Device endpoints
- api.GET("/devices", func(c *gin.Context) {
- handlers.GetDevices(c, cfg)
- })
-
- // Status endpoint
- api.GET("/status", func(c *gin.Context) {
- handlers.GetStatus(c, status)
- })
-
- // Log endpoints
- api.GET("/logs", handlers.GetLogs)
- api.DELETE("/logs", handlers.ClearLogs)
- }
-
- // WebSocket endpoints
- r.GET("/ws/logs", handlers.WebSocketLogs)
- r.GET("/ws/wifi", handlers.WebSocketWifi)
-
- // Serve static files
- sub, err := fs.Sub(assets, "static")
- if err != nil {
- panic("Unable to access static directory: " + err.Error())
- }
- r.NoRoute(gin.WrapH(http.FileServer(http.FS(sub))))
-
- return r
-}
diff --git a/internal/app/app.go b/internal/app/app.go
deleted file mode 100644
index 3e55b49..0000000
--- a/internal/app/app.go
+++ /dev/null
@@ -1,287 +0,0 @@
-package app
-
-import (
- "embed"
- "log"
- "os"
- "strconv"
- "strings"
- "sync"
- "time"
-
- "github.com/nemunaire/repeater/internal/api"
- "github.com/nemunaire/repeater/internal/config"
- "github.com/nemunaire/repeater/internal/hotspot"
- "github.com/nemunaire/repeater/internal/logging"
- "github.com/nemunaire/repeater/internal/models"
- "github.com/nemunaire/repeater/internal/station"
- "github.com/nemunaire/repeater/internal/station/backend"
- "github.com/nemunaire/repeater/internal/syslog"
- "github.com/nemunaire/repeater/internal/wifi"
-)
-
-// App represents the application
-type App struct {
- Status models.SystemStatus
- StatusMutex sync.RWMutex
- StartTime time.Time
- Assets embed.FS
- Config *config.Config
- SyslogTailer *syslog.SyslogTailer
-}
-
-// New creates a new application instance
-func New(assets embed.FS) *App {
- return &App{
- Status: models.SystemStatus{
- Connected: false,
- ConnectionState: "disconnected",
- ConnectedSSID: "",
- HotspotStatus: nil,
- ConnectedCount: 0,
- DataUsage: 0.0,
- Uptime: 0,
- },
- StartTime: time.Now(),
- Assets: assets,
- }
-}
-
-// Initialize initializes the application
-func (a *App) Initialize(cfg *config.Config) error {
- // Store config reference
- a.Config = cfg
-
- // Initialize WiFi backend
- if err := wifi.Initialize(cfg.WifiInterface, cfg.WifiBackend); err != nil {
- return err
- }
-
- // Start WiFi event monitoring
- if err := wifi.StartEventMonitoring(); err != nil {
- log.Printf("Warning: WiFi event monitoring failed: %v", err)
- // Don't fail - polling fallback still works
- }
-
- // Initialize station backend
- stationConfig := backend.BackendConfig{
- InterfaceName: cfg.HotspotInterface,
- ARPTablePath: cfg.ARPTablePath,
- DHCPLeasesPath: cfg.DHCPLeasesPath,
- HostapdInterface: cfg.HotspotInterface,
- }
- if err := station.Initialize(cfg.StationBackend, stationConfig); err != nil {
- log.Printf("Warning: Station backend initialization failed: %v", err)
- // Don't fail - will continue without station discovery
- } else {
- // Start event monitoring for station events
- if err := station.StartEventMonitoring(backend.EventCallbacks{
- OnStationConnected: a.handleStationConnected,
- OnStationDisconnected: a.handleStationDisconnected,
- OnStationUpdated: a.handleStationUpdated,
- }); err != nil {
- log.Printf("Warning: Station event monitoring failed: %v", err)
- // Don't fail - polling fallback still works
- }
- }
-
- // Start syslog tailing if enabled
- if cfg.SyslogEnabled {
- a.SyslogTailer = syslog.NewSyslogTailer(
- cfg.SyslogPath,
- cfg.SyslogFilter,
- cfg.SyslogSource,
- )
- if err := a.SyslogTailer.Start(); err != nil {
- log.Printf("Warning: Failed to start syslog tailing: %v", err)
- // Don't fail - app continues without syslog
- }
- }
-
- // Start periodic tasks
- go a.periodicStatusUpdate()
- go a.periodicDeviceUpdate()
-
- logging.AddLog("Système", "Application initialisée")
- return nil
-}
-
-// Run starts the HTTP server
-func (a *App) Run(addr string) error {
- router := api.SetupRouter(&a.Status, a.Config, a.Assets)
-
- logging.AddLog("Système", "Serveur API démarré sur "+addr)
- return router.Run(addr)
-}
-
-// Shutdown gracefully shuts down the application
-func (a *App) Shutdown() {
- // Stop syslog tailing if running
- if a.SyslogTailer != nil {
- a.SyslogTailer.Stop()
- }
-
- // Stop station monitoring and close backend
- station.StopEventMonitoring()
- station.Close()
-
- wifi.StopEventMonitoring()
- wifi.Close()
- logging.AddLog("Système", "Application arrêtée")
-}
-
-// getSystemUptime reads system uptime from /proc/uptime
-func getSystemUptime() int64 {
- data, err := os.ReadFile("/proc/uptime")
- if err != nil {
- log.Printf("Error reading /proc/uptime: %v", err)
- return 0
- }
-
- fields := strings.Fields(string(data))
- if len(fields) == 0 {
- return 0
- }
-
- uptime, err := strconv.ParseFloat(fields[0], 64)
- if err != nil {
- log.Printf("Error parsing uptime: %v", err)
- return 0
- }
-
- return int64(uptime)
-}
-
-// getInterfaceBytes reads rx and tx bytes for a network interface
-func getInterfaceBytes(interfaceName string) (rxBytes, txBytes int64) {
- rxPath := "/sys/class/net/" + interfaceName + "/statistics/rx_bytes"
- txPath := "/sys/class/net/" + interfaceName + "/statistics/tx_bytes"
-
- // Read RX bytes
- rxData, err := os.ReadFile(rxPath)
- if err != nil {
- log.Printf("Error reading rx_bytes for %s: %v", interfaceName, err)
- } else {
- rxBytes, _ = strconv.ParseInt(strings.TrimSpace(string(rxData)), 10, 64)
- }
-
- // Read TX bytes
- txData, err := os.ReadFile(txPath)
- if err != nil {
- log.Printf("Error reading tx_bytes for %s: %v", interfaceName, err)
- } else {
- txBytes, _ = strconv.ParseInt(strings.TrimSpace(string(txData)), 10, 64)
- }
-
- return rxBytes, txBytes
-}
-
-// periodicStatusUpdate updates WiFi connection status periodically
-func (a *App) periodicStatusUpdate() {
- ticker := time.NewTicker(5 * time.Second)
- defer ticker.Stop()
-
- for range ticker.C {
- a.StatusMutex.Lock()
- a.Status.Connected = wifi.IsConnected()
- a.Status.ConnectionState = wifi.GetConnectionState()
- a.Status.ConnectedSSID = wifi.GetConnectedSSID()
- a.Status.Uptime = getSystemUptime()
-
- // Get detailed hotspot status
- a.Status.HotspotStatus = hotspot.GetDetailedStatus()
-
- // Get network data usage for WiFi interface
- if a.Config != nil {
- rxBytes, txBytes := getInterfaceBytes(a.Config.WifiInterface)
- // Convert to MB and sum rx + tx
- a.Status.DataUsage = float64(rxBytes+txBytes) / (1024 * 1024)
- }
-
- a.StatusMutex.Unlock()
- }
-}
-
-// periodicDeviceUpdate updates connected devices list periodically
-func (a *App) periodicDeviceUpdate() {
- ticker := time.NewTicker(10 * time.Second)
- defer ticker.Stop()
-
- for range ticker.C {
- devices, err := station.GetStations()
- if err != nil {
- log.Printf("Error getting connected devices: %v", err)
- }
-
- a.StatusMutex.Lock()
- a.Status.ConnectedDevices = devices
- a.Status.ConnectedCount = len(devices)
- a.StatusMutex.Unlock()
- }
-}
-
-// handleStationConnected handles station connection events
-func (a *App) handleStationConnected(st backend.Station) {
- a.StatusMutex.Lock()
- defer a.StatusMutex.Unlock()
-
- // Convert backend.Station to models.ConnectedDevice
- device := models.ConnectedDevice{
- Name: st.Hostname,
- Type: st.Type,
- MAC: st.MAC,
- IP: st.IP,
- }
-
- // Check if device already exists
- found := false
- for i, d := range a.Status.ConnectedDevices {
- if d.MAC == device.MAC {
- a.Status.ConnectedDevices[i] = device
- found = true
- break
- }
- }
-
- // Add new device if not found
- if !found {
- a.Status.ConnectedDevices = append(a.Status.ConnectedDevices, device)
- a.Status.ConnectedCount = len(a.Status.ConnectedDevices)
- logging.AddLog("Stations", "Device connected: "+device.MAC+" ("+device.IP+")")
- }
-}
-
-// handleStationDisconnected handles station disconnection events
-func (a *App) handleStationDisconnected(mac string) {
- a.StatusMutex.Lock()
- defer a.StatusMutex.Unlock()
-
- // Remove device from list
- for i, d := range a.Status.ConnectedDevices {
- if d.MAC == mac {
- a.Status.ConnectedDevices = append(a.Status.ConnectedDevices[:i], a.Status.ConnectedDevices[i+1:]...)
- a.Status.ConnectedCount = len(a.Status.ConnectedDevices)
- logging.AddLog("Stations", "Device disconnected: "+mac)
- break
- }
- }
-}
-
-// handleStationUpdated handles station update events
-func (a *App) handleStationUpdated(st backend.Station) {
- a.StatusMutex.Lock()
- defer a.StatusMutex.Unlock()
-
- // Update existing device
- for i, d := range a.Status.ConnectedDevices {
- if d.MAC == st.MAC {
- a.Status.ConnectedDevices[i] = models.ConnectedDevice{
- Name: st.Hostname,
- Type: st.Type,
- MAC: st.MAC,
- IP: st.IP,
- }
- break
- }
- }
-}
diff --git a/internal/config/cli.go b/internal/config/cli.go
deleted file mode 100644
index 49013b3..0000000
--- a/internal/config/cli.go
+++ /dev/null
@@ -1,34 +0,0 @@
-package config
-
-import (
- "flag"
-)
-
-// declareFlags registers flags for the structure Options.
-func declareFlags(o *Config) {
- flag.StringVar(&o.Bind, "bind", ":8081", "Bind port/socket")
- flag.StringVar(&o.WifiInterface, "wifi-interface", "wlan0", "WiFi interface name")
- flag.StringVar(&o.HotspotInterface, "hotspot-interface", "wlan1", "Hotspot WiFi interface name")
- flag.StringVar(&o.WifiBackend, "wifi-backend", "", "WiFi backend to use: 'iwd' or 'wpasupplicant' (required)")
- flag.StringVar(&o.StationBackend, "station-backend", "hostapd", "Station discovery backend: 'arp', 'dhcp', or 'hostapd'")
- flag.StringVar(&o.DHCPLeasesPath, "dhcp-leases-path", "/var/lib/dhcp/dhcpd.leases", "Path to DHCP leases file")
- flag.StringVar(&o.ARPTablePath, "arp-table-path", "/proc/net/arp", "Path to ARP table file")
- flag.BoolVar(&o.SyslogEnabled, "syslog-enabled", false, "Enable syslog tailing for iwd messages")
- flag.StringVar(&o.SyslogPath, "syslog-path", "/var/log/messages", "Path to syslog file")
- flag.Var(&StringArray{&o.SyslogFilter}, "daemon.info iwd:", "Filter string for syslog lines")
- flag.StringVar(&o.SyslogSource, "syslog-source", "iwd", "Source name for syslog entries in logs")
-}
-
-// parseCLI parse the flags and treats extra args as configuration filename.
-func parseCLI(o *Config) error {
- flag.Parse()
-
- for _, conf := range flag.Args() {
- err := parseFile(o, conf)
- if err != nil {
- return err
- }
- }
-
- return nil
-}
diff --git a/internal/config/config.go b/internal/config/config.go
deleted file mode 100644
index 3cd2fa7..0000000
--- a/internal/config/config.go
+++ /dev/null
@@ -1,106 +0,0 @@
-package config
-
-import (
- "flag"
- "log"
- "os"
- "path"
- "strings"
-)
-
-type Config struct {
- Bind string
- WifiInterface string
- HotspotInterface string
- WifiBackend string
- StationBackend string // "arp", "dhcp", or "hostapd"
- DHCPLeasesPath string
- ARPTablePath string
- SyslogEnabled bool
- SyslogPath string
- SyslogFilter []string
- SyslogSource string
-}
-
-// ConsolidateConfig fills an Options struct by reading configuration from
-// config files, environment, then command line.
-//
-// Should be called only one time.
-func ConsolidateConfig() (opts *Config, err error) {
- // Define defaults options
- opts = &Config{
- Bind: ":8080",
- WifiInterface: "wlan0",
- HotspotInterface: "wlan1",
- DHCPLeasesPath: "/var/lib/dhcp/dhcpd.leases",
- ARPTablePath: "/proc/net/arp",
- SyslogEnabled: false,
- SyslogPath: "/var/log/messages",
- SyslogFilter: []string{"daemon.info wpa_supplicant:", "daemon.info iwd:", "daemon.info hostapd:"},
- SyslogSource: "iwd",
- }
-
- declareFlags(opts)
-
- // Establish a list of possible configuration file locations
- configLocations := []string{
- "repeater.conf",
- }
-
- if home, err := os.UserConfigDir(); err == nil {
- configLocations = append(configLocations, path.Join(home, "repeater", "repeater.conf"))
- }
-
- configLocations = append(configLocations, path.Join("etc", "repeater.conf"))
-
- // If config file exists, read configuration from it
- for _, filename := range configLocations {
- if _, e := os.Stat(filename); !os.IsNotExist(e) {
- log.Printf("Loading configuration from %s\n", filename)
- err = parseFile(opts, filename)
- if err != nil {
- return
- }
- break
- }
- }
-
- // Then, overwrite that by what is present in the environment
- err = parseEnvironmentVariables(opts)
- if err != nil {
- return
- }
-
- // Finaly, command line takes precedence
- err = parseCLI(opts)
- if err != nil {
- return
- }
-
- // Validate configuration
- if opts.WifiBackend != "iwd" && opts.WifiBackend != "wpasupplicant" {
- log.Fatalf("wifi-backend must be set to 'iwd' or 'wpasupplicant' (got: '%s')", opts.WifiBackend)
- }
-
- if opts.StationBackend != "arp" && opts.StationBackend != "dhcp" && opts.StationBackend != "hostapd" {
- log.Fatalf("station-backend must be set to 'arp', 'dhcp', or 'hostapd' (got: '%s')", opts.StationBackend)
- }
-
- return
-}
-
-// parseLine treats a config line and place the read value in the variable
-// declared to the corresponding flag.
-func parseLine(o *Config, line string) (err error) {
- fields := strings.SplitN(line, "=", 2)
- orig_key := strings.TrimSpace(fields[0])
- value := strings.TrimSpace(fields[1])
-
- key := strings.TrimPrefix(orig_key, "REPEATER_")
- key = strings.Replace(key, "_", "-", -1)
- key = strings.ToLower(key)
-
- err = flag.Set(key, value)
-
- return
-}
diff --git a/internal/config/custom.go b/internal/config/custom.go
deleted file mode 100644
index f038b7e..0000000
--- a/internal/config/custom.go
+++ /dev/null
@@ -1,45 +0,0 @@
-package config
-
-import (
- "fmt"
- "net/url"
-)
-
-// StringArray is a custom type for handling multiple string values in flags.
-type StringArray struct {
- Array *[]string
-}
-
-// String returns a string representation of the StringArray.
-func (i *StringArray) String() string {
- return fmt.Sprintf("%v", i.Array)
-}
-
-// Set appends a new string value to the StringArray.
-func (i *StringArray) Set(value string) error {
- *i.Array = append(*i.Array, value)
-
- return nil
-}
-
-type URL struct {
- URL *url.URL
-}
-
-func (i *URL) String() string {
- if i.URL != nil {
- return i.URL.String()
- } else {
- return ""
- }
-}
-
-func (i *URL) Set(value string) error {
- u, err := url.Parse(value)
- if err != nil {
- return err
- }
-
- *i.URL = *u
- return nil
-}
diff --git a/internal/config/env.go b/internal/config/env.go
deleted file mode 100644
index 8888746..0000000
--- a/internal/config/env.go
+++ /dev/null
@@ -1,21 +0,0 @@
-package config
-
-import (
- "fmt"
- "os"
- "strings"
-)
-
-// parseEnvironmentVariables analyzes all the environment variables to find
-// each one starting by REPEATER_
-func parseEnvironmentVariables(o *Config) (err error) {
- for _, line := range os.Environ() {
- if strings.HasPrefix(line, "REPEATER_") {
- err := parseLine(o, line)
- if err != nil {
- return fmt.Errorf("error in environment (%q): %w", line, err)
- }
- }
- }
- return
-}
diff --git a/internal/config/file.go b/internal/config/file.go
deleted file mode 100644
index 8ba6a10..0000000
--- a/internal/config/file.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package config
-
-import (
- "bufio"
- "fmt"
- "os"
- "strings"
-)
-
-// parseFile opens the file at the given filename path, then treat each line
-// not starting with '#' as a configuration statement.
-func parseFile(o *Config, filename string) error {
- fp, err := os.Open(filename)
- if err != nil {
- return err
- }
- defer fp.Close()
-
- scanner := bufio.NewScanner(fp)
- n := 0
- for scanner.Scan() {
- n += 1
- line := strings.TrimSpace(scanner.Text())
- if len(line) > 0 && !strings.HasPrefix(line, "#") && strings.Index(line, "=") > 0 {
- err := parseLine(o, line)
- if err != nil {
- return fmt.Errorf("%v:%d: error in configuration: %w", filename, n, err)
- }
- }
- }
-
- return nil
-}
diff --git a/internal/hotspot/hotspot.go b/internal/hotspot/hotspot.go
deleted file mode 100644
index 793352f..0000000
--- a/internal/hotspot/hotspot.go
+++ /dev/null
@@ -1,111 +0,0 @@
-package hotspot
-
-import (
- "fmt"
- "os"
- "os/exec"
- "strconv"
- "strings"
-
- "github.com/nemunaire/repeater/internal/models"
-)
-
-const (
- AP_INTERFACE = "wlan1"
- HOSTAPD_CONF = "/etc/hostapd/hostapd.conf"
-)
-
-// Configure updates the hotspot configuration
-func Configure(config models.HotspotConfig) error {
- hostapdConfig := fmt.Sprintf(`interface=%s
-driver=nl80211
-ssid=%s
-hw_mode=g
-channel=%d
-wmm_enabled=0
-macaddr_acl=0
-auth_algs=1
-ignore_broadcast_ssid=0
-wpa=2
-wpa_passphrase=%s
-wpa_key_mgmt=WPA-PSK
-wpa_pairwise=TKIP
-rsn_pairwise=CCMP
-`, AP_INTERFACE, config.SSID, config.Channel, config.Password)
-
- return os.WriteFile(HOSTAPD_CONF, []byte(hostapdConfig), 0644)
-}
-
-// Start starts the hotspot
-func Start() error {
- cmd := exec.Command("/etc/init.d/hostapd", "start")
- return cmd.Run()
-}
-
-// Stop stops the hotspot
-func Stop() error {
- cmd := exec.Command("/etc/init.d/hostapd", "stop")
- return cmd.Run()
-}
-
-// Status checks if the hotspot is running.
-// Returns nil if the service is running, or an error if it's stopped or crashed.
-func Status() error {
- cmd := exec.Command("/etc/init.d/hostapd", "status")
- return cmd.Run()
-}
-
-// GetDetailedStatus retrieves detailed status information from hostapd_cli.
-// Returns nil if hostapd is not running or if there's an error.
-func GetDetailedStatus() *models.HotspotStatus {
- cmd := exec.Command("hostapd_cli", "status")
- output, err := cmd.Output()
- if err != nil {
- return nil
- }
-
- status := &models.HotspotStatus{}
- lines := strings.Split(string(output), "\n")
-
- for _, line := range lines {
- line = strings.TrimSpace(line)
- if line == "" || strings.HasPrefix(line, "Selected interface") {
- continue
- }
-
- parts := strings.SplitN(line, "=", 2)
- if len(parts) != 2 {
- continue
- }
-
- key := strings.TrimSpace(parts[0])
- value := strings.TrimSpace(parts[1])
-
- switch key {
- case "state":
- status.State = value
- case "channel":
- if ch, err := strconv.Atoi(value); err == nil {
- status.Channel = ch
- }
- case "freq":
- if freq, err := strconv.Atoi(value); err == nil {
- status.Frequency = freq
- }
- case "ssid[0]":
- status.SSID = value
- case "bssid[0]":
- status.BSSID = value
- case "num_sta[0]":
- if num, err := strconv.Atoi(value); err == nil {
- status.NumStations = num
- }
- case "hw_mode":
- status.HWMode = value
- case "country_code":
- status.CountryCode = value
- }
- }
-
- return status
-}
diff --git a/internal/logging/logging.go b/internal/logging/logging.go
deleted file mode 100644
index 064a813..0000000
--- a/internal/logging/logging.go
+++ /dev/null
@@ -1,94 +0,0 @@
-package logging
-
-import (
- "log"
- "sync"
- "time"
-
- "github.com/gorilla/websocket"
- "github.com/nemunaire/repeater/internal/models"
-)
-
-var (
- logEntries []models.LogEntry
- logMutex sync.RWMutex
- websocketClients = make(map[*websocket.Conn]bool)
- clientsMutex sync.RWMutex
-)
-
-// AddLog adds a new log entry
-func AddLog(source, message string) {
- logMutex.Lock()
- entry := models.LogEntry{
- Timestamp: time.Now(),
- Source: source,
- Message: message,
- }
- logEntries = append(logEntries, entry)
-
- // Keep only the last 100 logs
- if len(logEntries) > 100 {
- logEntries = logEntries[len(logEntries)-100:]
- }
- logMutex.Unlock()
-
- // Broadcast to WebSocket clients
- broadcastToWebSockets(entry)
-
- // Log to console
- log.Printf("[%s] %s", source, message)
-}
-
-// GetLogs returns all log entries
-func GetLogs() []models.LogEntry {
- logMutex.RLock()
- defer logMutex.RUnlock()
-
- logs := make([]models.LogEntry, len(logEntries))
- copy(logs, logEntries)
- return logs
-}
-
-// ClearLogs clears all log entries
-func ClearLogs() {
- logMutex.Lock()
- logEntries = []models.LogEntry{}
- logMutex.Unlock()
-
- AddLog("Système", "Logs effacés")
-}
-
-// RegisterWebSocketClient registers a new WebSocket client
-func RegisterWebSocketClient(conn *websocket.Conn) {
- clientsMutex.Lock()
- websocketClients[conn] = true
- clientsMutex.Unlock()
-
- // Send existing logs to the new client
- logMutex.RLock()
- for _, entry := range logEntries {
- conn.WriteJSON(entry)
- }
- logMutex.RUnlock()
-}
-
-// UnregisterWebSocketClient removes a WebSocket client
-func UnregisterWebSocketClient(conn *websocket.Conn) {
- clientsMutex.Lock()
- delete(websocketClients, conn)
- clientsMutex.Unlock()
-}
-
-// broadcastToWebSockets sends a log entry to all connected WebSocket clients
-func broadcastToWebSockets(entry models.LogEntry) {
- clientsMutex.RLock()
- defer clientsMutex.RUnlock()
-
- for client := range websocketClients {
- err := client.WriteJSON(entry)
- if err != nil {
- client.Close()
- delete(websocketClients, client)
- }
- }
-}
diff --git a/internal/models/models.go b/internal/models/models.go
deleted file mode 100644
index 64d5c79..0000000
--- a/internal/models/models.go
+++ /dev/null
@@ -1,71 +0,0 @@
-package models
-
-import "time"
-
-// WiFiNetwork represents a discovered WiFi network
-type WiFiNetwork struct {
- SSID string `json:"ssid"`
- Signal int `json:"signal"`
- Security string `json:"security"`
- Channel int `json:"channel"`
- BSSID string `json:"bssid"`
-}
-
-// ConnectedDevice represents a device connected to the hotspot
-type ConnectedDevice struct {
- Name string `json:"name"`
- Type string `json:"type"`
- MAC string `json:"mac"`
- IP string `json:"ip"`
-}
-
-// HotspotConfig represents hotspot configuration
-type HotspotConfig struct {
- SSID string `json:"ssid"`
- Password string `json:"password"`
- Channel int `json:"channel"`
-}
-
-// HotspotStatus represents detailed hotspot status
-type HotspotStatus struct {
- State string `json:"state"` // ENABLED, DISABLED, etc.
- SSID string `json:"ssid"` // Current SSID being broadcast
- BSSID string `json:"bssid"` // MAC address of the AP
- Channel int `json:"channel"` // Current channel
- Frequency int `json:"frequency"` // Frequency in MHz
- NumStations int `json:"numStations"` // Number of connected stations
- HWMode string `json:"hwMode"` // Hardware mode (g, a, n, ac, etc.)
- CountryCode string `json:"countryCode"` // Country code
-}
-
-// SystemStatus represents overall system status
-type SystemStatus struct {
- Connected bool `json:"connected"`
- ConnectionState string `json:"connectionState"` // Connection state: connected, disconnected, connecting, disconnecting, roaming
- ConnectedSSID string `json:"connectedSSID"`
- HotspotStatus *HotspotStatus `json:"hotspotStatus,omitempty"` // Detailed hotspot status
- ConnectedCount int `json:"connectedCount"`
- DataUsage float64 `json:"dataUsage"`
- Uptime int64 `json:"uptime"`
- ConnectedDevices []ConnectedDevice `json:"connectedDevices"`
-}
-
-// WiFiConnectRequest represents a request to connect to WiFi
-type WiFiConnectRequest struct {
- SSID string `json:"ssid" binding:"required"`
- Password string `json:"password"`
-}
-
-// LogEntry represents a system log entry
-type LogEntry struct {
- Timestamp time.Time `json:"timestamp"`
- Source string `json:"source"`
- Message string `json:"message"`
-}
-
-// DHCPLease represents a DHCP lease entry
-type DHCPLease struct {
- IP string
- MAC string
- Hostname string
-}
diff --git a/internal/station/arp/backend.go b/internal/station/arp/backend.go
deleted file mode 100644
index 5b4e975..0000000
--- a/internal/station/arp/backend.go
+++ /dev/null
@@ -1,177 +0,0 @@
-package arp
-
-import (
- "sync"
- "time"
-
- "github.com/nemunaire/repeater/internal/station/backend"
-)
-
-// Backend implements StationBackend using ARP table discovery
-type Backend struct {
- arpTablePath string
- lastStations map[string]backend.Station // Key: MAC address
- callbacks backend.EventCallbacks
- stopChan chan struct{}
- mu sync.RWMutex
- running bool
-}
-
-// NewBackend creates a new ARP backend
-func NewBackend() *Backend {
- return &Backend{
- lastStations: make(map[string]backend.Station),
- stopChan: make(chan struct{}),
- }
-}
-
-// Initialize initializes the ARP backend
-func (b *Backend) Initialize(config backend.BackendConfig) error {
- b.mu.Lock()
- defer b.mu.Unlock()
-
- b.arpTablePath = config.ARPTablePath
- if b.arpTablePath == "" {
- b.arpTablePath = "/proc/net/arp"
- }
-
- return nil
-}
-
-// Close cleans up backend resources
-func (b *Backend) Close() error {
- b.StopEventMonitoring()
- return nil
-}
-
-// GetStations returns all connected stations from ARP table
-func (b *Backend) GetStations() ([]backend.Station, error) {
- b.mu.RLock()
- arpTablePath := b.arpTablePath
- b.mu.RUnlock()
-
- arpEntries, err := parseARPTable(arpTablePath)
- if err != nil {
- return nil, err
- }
-
- var stations []backend.Station
- for _, entry := range arpEntries {
- // Only include entries with valid flags (2 = COMPLETE, 6 = COMPLETE|PERM)
- if entry.Flags == 2 || entry.Flags == 6 {
- st := backend.Station{
- MAC: entry.HWAddress.String(),
- IP: entry.IP.String(),
- Hostname: "", // No hostname available from ARP
- Type: backend.GuessDeviceType("", entry.HWAddress.String()),
- Signal: 0, // Not available from ARP
- RxBytes: 0, // Not available from ARP
- TxBytes: 0, // Not available from ARP
- ConnectedAt: time.Now(),
- }
- stations = append(stations, st)
- }
- }
-
- return stations, nil
-}
-
-// StartEventMonitoring starts monitoring for station events via polling
-func (b *Backend) StartEventMonitoring(callbacks backend.EventCallbacks) error {
- b.mu.Lock()
- defer b.mu.Unlock()
-
- if b.running {
- return nil
- }
-
- b.callbacks = callbacks
- b.running = true
-
- // Start polling goroutine
- go b.pollLoop()
-
- return nil
-}
-
-// StopEventMonitoring stops event monitoring
-func (b *Backend) StopEventMonitoring() {
- b.mu.Lock()
- if !b.running {
- b.mu.Unlock()
- return
- }
- b.running = false
- b.mu.Unlock()
-
- close(b.stopChan)
-}
-
-// SupportsRealTimeEvents returns false (ARP is polling-based)
-func (b *Backend) SupportsRealTimeEvents() bool {
- return false
-}
-
-// pollLoop polls the ARP table and simulates events
-func (b *Backend) pollLoop() {
- ticker := time.NewTicker(10 * time.Second)
- defer ticker.Stop()
-
- for {
- select {
- case <-ticker.C:
- b.checkForChanges()
- case <-b.stopChan:
- return
- }
- }
-}
-
-// checkForChanges compares current state with last state and triggers callbacks
-func (b *Backend) checkForChanges() {
- // Get current stations
- current, err := b.GetStations()
- if err != nil {
- return
- }
-
- // Build map of current stations
- currentMap := make(map[string]backend.Station)
- for _, station := range current {
- currentMap[station.MAC] = station
- }
-
- b.mu.Lock()
- defer b.mu.Unlock()
-
- // Check for new stations (connected)
- for mac, station := range currentMap {
- if _, exists := b.lastStations[mac]; !exists {
- // New station connected
- if b.callbacks.OnStationConnected != nil {
- go b.callbacks.OnStationConnected(station)
- }
- } else {
- // Check for updates (IP change, etc.)
- oldStation := b.lastStations[mac]
- if oldStation.IP != station.IP || oldStation.Hostname != station.Hostname {
- if b.callbacks.OnStationUpdated != nil {
- go b.callbacks.OnStationUpdated(station)
- }
- }
- }
- }
-
- // Check for disconnected stations
- for mac := range b.lastStations {
- if _, exists := currentMap[mac]; !exists {
- // Station disconnected
- if b.callbacks.OnStationDisconnected != nil {
- go b.callbacks.OnStationDisconnected(mac)
- }
- }
- }
-
- // Update last state
- b.lastStations = currentMap
-}
diff --git a/internal/station/arp/parser.go b/internal/station/arp/parser.go
deleted file mode 100644
index ca40f01..0000000
--- a/internal/station/arp/parser.go
+++ /dev/null
@@ -1,64 +0,0 @@
-package arp
-
-import (
- "fmt"
- "net"
- "os"
- "strings"
-)
-
-// ARPEntry represents an entry in the ARP table
-type ARPEntry struct {
- IP net.IP
- HWType int
- Flags int
- HWAddress net.HardwareAddr
- Mask string
- Device string
-}
-
-// parseARPTable reads and parses ARP table from /proc/net/arp format
-func parseARPTable(path string) ([]ARPEntry, error) {
- var entries []ARPEntry
-
- content, err := os.ReadFile(path)
- if err != nil {
- return entries, err
- }
-
- for _, line := range strings.Split(string(content), "\n") {
- fields := strings.Fields(line)
- if len(fields) > 5 {
- var entry ARPEntry
-
- // Parse HWType (hex format)
- if _, err := fmt.Sscanf(fields[1], "0x%x", &entry.HWType); err != nil {
- continue
- }
-
- // Parse Flags (hex format)
- if _, err := fmt.Sscanf(fields[2], "0x%x", &entry.Flags); err != nil {
- continue
- }
-
- // Parse IP address
- entry.IP = net.ParseIP(fields[0])
- if entry.IP == nil {
- continue
- }
-
- // Parse MAC address
- entry.HWAddress, err = net.ParseMAC(fields[3])
- if err != nil {
- continue
- }
-
- entry.Mask = fields[4]
- entry.Device = fields[5]
-
- entries = append(entries, entry)
- }
- }
-
- return entries, nil
-}
diff --git a/internal/station/backend/types.go b/internal/station/backend/types.go
deleted file mode 100644
index 9874d5c..0000000
--- a/internal/station/backend/types.go
+++ /dev/null
@@ -1,99 +0,0 @@
-package backend
-
-import (
- "strings"
- "time"
-)
-
-// StationBackend defines the interface for station/device discovery backends.
-// Implementations include ARP-based, DHCP-based, and Hostapd DBus-based discovery.
-type StationBackend interface {
- // Initialize initializes the backend with the given configuration
- Initialize(config BackendConfig) error
-
- // Close cleans up backend resources
- Close() error
-
- // GetStations returns all currently connected stations
- GetStations() ([]Station, error)
-
- // StartEventMonitoring starts monitoring for station events
- // Backends that don't support real-time events will poll and simulate events
- StartEventMonitoring(callbacks EventCallbacks) error
-
- // StopEventMonitoring stops event monitoring
- StopEventMonitoring()
-
- // SupportsRealTimeEvents returns true if backend supports real-time events (e.g., DBus)
- // Returns false for polling-based backends (ARP, DHCP)
- SupportsRealTimeEvents() bool
-}
-
-// Station represents a connected device in a backend-agnostic format
-type Station struct {
- MAC string // Hardware MAC address (required, primary identifier)
- IP string // IP address (may be empty for some backends initially)
- Hostname string // Device hostname (may be empty)
- Type string // Device type: "mobile", "laptop", "tablet", "unknown"
- Signal int32 // Signal strength in dBm (0 if not available)
- RxBytes uint64 // Received bytes (0 if not available)
- TxBytes uint64 // Transmitted bytes (0 if not available)
- ConnectedAt time.Time // When station connected (best effort)
-}
-
-// EventCallbacks defines callback functions for station events.
-// Backends call these when stations connect, disconnect, or update.
-type EventCallbacks struct {
- // OnStationConnected is called when a new station connects
- OnStationConnected func(station Station)
-
- // OnStationDisconnected is called when a station disconnects
- OnStationDisconnected func(mac string)
-
- // OnStationUpdated is called when station information changes
- // (e.g., IP discovered, signal strength changed)
- OnStationUpdated func(station Station)
-}
-
-// BackendConfig provides configuration for backend initialization
-type BackendConfig struct {
- // Common
- InterfaceName string // Network interface (e.g., "wlan1")
-
- // ARP-specific
- ARPTablePath string // Path to /proc/net/arp (default: "/proc/net/arp")
-
- // DHCP-specific
- DHCPLeasesPath string // Path to DHCP leases file
-
- // Hostapd-specific
- HostapdInterface string // Hostapd interface name for DBus
-}
-
-// GuessDeviceType attempts to guess device type from hostname and MAC address
-func GuessDeviceType(hostname, mac string) string {
- hostname = strings.ToLower(hostname)
-
- if strings.Contains(hostname, "iphone") || strings.Contains(hostname, "android") {
- return "mobile"
- } else if strings.Contains(hostname, "ipad") || strings.Contains(hostname, "tablet") {
- return "tablet"
- } else if strings.Contains(hostname, "macbook") || strings.Contains(hostname, "laptop") {
- return "laptop"
- }
-
- // Guess by MAC prefix (OUI)
- if len(mac) >= 8 {
- macPrefix := strings.ToUpper(mac[:8])
- switch macPrefix {
- case "00:50:56", "00:0C:29", "00:05:69": // VMware
- return "laptop"
- case "08:00:27": // VirtualBox
- return "laptop"
- default:
- return "mobile"
- }
- }
-
- return "unknown"
-}
diff --git a/internal/station/dhcp/backend.go b/internal/station/dhcp/backend.go
deleted file mode 100644
index 54abf95..0000000
--- a/internal/station/dhcp/backend.go
+++ /dev/null
@@ -1,184 +0,0 @@
-package dhcp
-
-import (
- "sync"
- "time"
-
- "github.com/nemunaire/repeater/internal/station/backend"
-)
-
-// Backend implements StationBackend using DHCP lease discovery
-type Backend struct {
- dhcpLeasesPath string
- lastStations map[string]backend.Station // Key: MAC address
- callbacks backend.EventCallbacks
- stopChan chan struct{}
- mu sync.RWMutex
- running bool
-}
-
-// NewBackend creates a new DHCP backend
-func NewBackend() *Backend {
- return &Backend{
- lastStations: make(map[string]backend.Station),
- stopChan: make(chan struct{}),
- }
-}
-
-// Initialize initializes the DHCP backend
-func (b *Backend) Initialize(config backend.BackendConfig) error {
- b.mu.Lock()
- defer b.mu.Unlock()
-
- b.dhcpLeasesPath = config.DHCPLeasesPath
- if b.dhcpLeasesPath == "" {
- b.dhcpLeasesPath = "/var/lib/dhcp/dhcpd.leases"
- }
-
- return nil
-}
-
-// Close cleans up backend resources
-func (b *Backend) Close() error {
- b.StopEventMonitoring()
- return nil
-}
-
-// GetStations returns all connected stations from DHCP leases validated by ARP
-func (b *Backend) GetStations() ([]backend.Station, error) {
- b.mu.RLock()
- dhcpLeasesPath := b.dhcpLeasesPath
- b.mu.RUnlock()
-
- // Read DHCP leases
- leases, err := parseDHCPLeases(dhcpLeasesPath)
- if err != nil {
- return nil, err
- }
-
- // Get ARP information for validation
- arpInfo, err := getARPInfo()
- if err != nil {
- return nil, err
- }
-
- var stations []backend.Station
- for _, lease := range leases {
- // Check if the device is still connected via ARP
- if _, exists := arpInfo[lease.IP]; exists {
- st := backend.Station{
- MAC: lease.MAC,
- IP: lease.IP,
- Hostname: lease.Hostname,
- Type: backend.GuessDeviceType(lease.Hostname, lease.MAC),
- Signal: 0, // Not available from DHCP
- RxBytes: 0, // Not available from DHCP
- TxBytes: 0, // Not available from DHCP
- ConnectedAt: time.Now(),
- }
- stations = append(stations, st)
- }
- }
-
- return stations, nil
-}
-
-// StartEventMonitoring starts monitoring for station events via polling
-func (b *Backend) StartEventMonitoring(callbacks backend.EventCallbacks) error {
- b.mu.Lock()
- defer b.mu.Unlock()
-
- if b.running {
- return nil
- }
-
- b.callbacks = callbacks
- b.running = true
-
- // Start polling goroutine
- go b.pollLoop()
-
- return nil
-}
-
-// StopEventMonitoring stops event monitoring
-func (b *Backend) StopEventMonitoring() {
- b.mu.Lock()
- if !b.running {
- b.mu.Unlock()
- return
- }
- b.running = false
- b.mu.Unlock()
-
- close(b.stopChan)
-}
-
-// SupportsRealTimeEvents returns false (DHCP is polling-based)
-func (b *Backend) SupportsRealTimeEvents() bool {
- return false
-}
-
-// pollLoop polls DHCP leases and simulates events
-func (b *Backend) pollLoop() {
- ticker := time.NewTicker(10 * time.Second)
- defer ticker.Stop()
-
- for {
- select {
- case <-ticker.C:
- b.checkForChanges()
- case <-b.stopChan:
- return
- }
- }
-}
-
-// checkForChanges compares current state with last state and triggers callbacks
-func (b *Backend) checkForChanges() {
- // Get current stations
- current, err := b.GetStations()
- if err != nil {
- return
- }
-
- // Build map of current stations
- currentMap := make(map[string]backend.Station)
- for _, st := range current {
- currentMap[st.MAC] = st
- }
-
- b.mu.Lock()
- defer b.mu.Unlock()
-
- // Check for new stations (connected)
- for mac, st := range currentMap {
- if _, exists := b.lastStations[mac]; !exists {
- // New station connected
- if b.callbacks.OnStationConnected != nil {
- go b.callbacks.OnStationConnected(st)
- }
- } else {
- // Check for updates (IP change, hostname change, etc.)
- oldStation := b.lastStations[mac]
- if oldStation.IP != st.IP || oldStation.Hostname != st.Hostname {
- if b.callbacks.OnStationUpdated != nil {
- go b.callbacks.OnStationUpdated(st)
- }
- }
- }
- }
-
- // Check for disconnected stations
- for mac := range b.lastStations {
- if _, exists := currentMap[mac]; !exists {
- // Station disconnected
- if b.callbacks.OnStationDisconnected != nil {
- go b.callbacks.OnStationDisconnected(mac)
- }
- }
- }
-
- // Update last state
- b.lastStations = currentMap
-}
diff --git a/internal/station/dhcp/parser.go b/internal/station/dhcp/parser.go
deleted file mode 100644
index efcd583..0000000
--- a/internal/station/dhcp/parser.go
+++ /dev/null
@@ -1,72 +0,0 @@
-package dhcp
-
-import (
- "bufio"
- "os"
- "os/exec"
- "regexp"
- "strings"
-
- "github.com/nemunaire/repeater/internal/models"
-)
-
-// parseDHCPLeases reads and parses DHCP lease file
-func parseDHCPLeases(path string) ([]models.DHCPLease, error) {
- var leases []models.DHCPLease
-
- file, err := os.Open(path)
- if err != nil {
- return leases, err
- }
- defer file.Close()
-
- scanner := bufio.NewScanner(file)
- var currentLease models.DHCPLease
-
- for scanner.Scan() {
- line := strings.TrimSpace(scanner.Text())
-
- if strings.HasPrefix(line, "lease ") {
- ip := strings.Fields(line)[1]
- currentLease = models.DHCPLease{IP: ip}
- } else if strings.Contains(line, "hardware ethernet") {
- mac := strings.Fields(line)[2]
- mac = strings.TrimSuffix(mac, ";")
- currentLease.MAC = mac
- } else if strings.Contains(line, "client-hostname") {
- hostname := strings.Fields(line)[1]
- hostname = strings.Trim(hostname, `";`)
- currentLease.Hostname = hostname
- } else if line == "}" {
- if currentLease.IP != "" && currentLease.MAC != "" {
- leases = append(leases, currentLease)
- }
- currentLease = models.DHCPLease{}
- }
- }
-
- return leases, nil
-}
-
-// getARPInfo retrieves ARP table information using arp command
-// Returns a map of IP -> MAC address
-func getARPInfo() (map[string]string, error) {
- arpInfo := make(map[string]string)
-
- cmd := exec.Command("arp", "-a")
- output, err := cmd.Output()
- if err != nil {
- return arpInfo, err
- }
-
- lines := strings.Split(string(output), "\n")
- for _, line := range lines {
- if matches := regexp.MustCompile(`\(([^)]+)\) at ([0-9a-fA-F:]{17})`).FindStringSubmatch(line); len(matches) > 2 {
- ip := matches[1]
- mac := matches[2]
- arpInfo[ip] = mac
- }
- }
-
- return arpInfo, nil
-}
diff --git a/internal/station/factory.go b/internal/station/factory.go
deleted file mode 100644
index 94bfc2b..0000000
--- a/internal/station/factory.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package station
-
-import (
- "fmt"
-
- "github.com/nemunaire/repeater/internal/station/arp"
- "github.com/nemunaire/repeater/internal/station/backend"
- "github.com/nemunaire/repeater/internal/station/dhcp"
- "github.com/nemunaire/repeater/internal/station/hostapd"
-)
-
-// createBackend creates a station backend based on the backend name
-func createBackend(backendName string) (backend.StationBackend, error) {
- switch backendName {
- case "arp":
- return arp.NewBackend(), nil
- case "dhcp":
- return dhcp.NewBackend(), nil
- case "hostapd":
- return hostapd.NewBackend(), nil
- default:
- return nil, fmt.Errorf("invalid station backend: %s (must be 'arp', 'dhcp', or 'hostapd')", backendName)
- }
-}
diff --git a/internal/station/hostapd/backend.go b/internal/station/hostapd/backend.go
deleted file mode 100644
index ce5708a..0000000
--- a/internal/station/hostapd/backend.go
+++ /dev/null
@@ -1,345 +0,0 @@
-package hostapd
-
-import (
- "bufio"
- "bytes"
- "fmt"
- "log"
- "os/exec"
- "strconv"
- "strings"
- "sync"
- "time"
-
- "github.com/nemunaire/repeater/internal/station/backend"
-)
-
-// Backend implements StationBackend using hostapd_cli
-type Backend struct {
- interfaceName string
- hostapdCLI string // Path to hostapd_cli executable
-
- stations map[string]*HostapdStation // Key: MAC address
- callbacks backend.EventCallbacks
-
- mu sync.RWMutex
- running bool
- stopCh chan struct{}
-
- // IP correlation - will be populated by periodic DHCP lease correlation
- ipByMAC map[string]string // MAC -> IP mapping
-}
-
-// NewBackend creates a new hostapd backend
-func NewBackend() *Backend {
- return &Backend{
- stations: make(map[string]*HostapdStation),
- ipByMAC: make(map[string]string),
- hostapdCLI: "hostapd_cli",
- stopCh: make(chan struct{}),
- }
-}
-
-// Initialize initializes the hostapd backend
-func (b *Backend) Initialize(config backend.BackendConfig) error {
- b.mu.Lock()
- defer b.mu.Unlock()
-
- b.interfaceName = config.InterfaceName
- if b.interfaceName == "" {
- b.interfaceName = "wlan1" // Default AP interface
- }
-
- // Check if hostapd_cli is available
- if _, err := exec.LookPath(b.hostapdCLI); err != nil {
- return fmt.Errorf("hostapd_cli not found in PATH: %w", err)
- }
-
- // Verify we can communicate with hostapd
- if err := b.runCommand("ping"); err != nil {
- return fmt.Errorf("failed to communicate with hostapd on interface %s: %w", b.interfaceName, err)
- }
-
- log.Printf("Hostapd backend initialized for interface %s", b.interfaceName)
-
- // Load initial station list
- if err := b.loadStations(); err != nil {
- log.Printf("Warning: Failed to load initial stations: %v", err)
- }
-
- return nil
-}
-
-// Close cleans up backend resources
-func (b *Backend) Close() error {
- b.StopEventMonitoring()
- return nil
-}
-
-// runCommand executes a hostapd_cli command and returns the output
-func (b *Backend) runCommand(args ...string) error {
- cmdArgs := []string{"-i", b.interfaceName}
- cmdArgs = append(cmdArgs, args...)
- cmd := exec.Command(b.hostapdCLI, cmdArgs...)
- return cmd.Run()
-}
-
-// runCommandOutput executes a hostapd_cli command and returns the output
-func (b *Backend) runCommandOutput(args ...string) (string, error) {
- cmdArgs := []string{"-i", b.interfaceName}
- cmdArgs = append(cmdArgs, args...)
- cmd := exec.Command(b.hostapdCLI, cmdArgs...)
- out, err := cmd.Output()
- if err != nil {
- return "", err
- }
- return string(out), nil
-}
-
-// GetStations returns all connected stations
-func (b *Backend) GetStations() ([]backend.Station, error) {
- b.mu.RLock()
- defer b.mu.RUnlock()
-
- stations := make([]backend.Station, 0, len(b.stations))
- for mac, hs := range b.stations {
- station := b.convertStation(mac, hs)
- stations = append(stations, station)
- }
-
- return stations, nil
-}
-
-// StartEventMonitoring starts monitoring for station events via polling
-func (b *Backend) StartEventMonitoring(callbacks backend.EventCallbacks) error {
- b.mu.Lock()
- defer b.mu.Unlock()
-
- if b.running {
- return nil
- }
-
- b.callbacks = callbacks
- b.running = true
-
- // Start polling goroutine
- go b.pollStations()
-
- log.Printf("Hostapd event monitoring started (polling mode)")
- return nil
-}
-
-// StopEventMonitoring stops event monitoring
-func (b *Backend) StopEventMonitoring() {
- b.mu.Lock()
- if !b.running {
- b.mu.Unlock()
- return
- }
- b.running = false
- b.mu.Unlock()
-
- close(b.stopCh)
-}
-
-// SupportsRealTimeEvents returns false (hostapd_cli uses polling)
-func (b *Backend) SupportsRealTimeEvents() bool {
- return false
-}
-
-// pollStations periodically polls for station changes
-func (b *Backend) pollStations() {
- ticker := time.NewTicker(2 * time.Second)
- defer ticker.Stop()
-
- for {
- select {
- case <-b.stopCh:
- return
- case <-ticker.C:
- if err := b.checkStationChanges(); err != nil {
- log.Printf("Error polling stations: %v", err)
- }
- }
- }
-}
-
-// checkStationChanges checks for station connect/disconnect events
-func (b *Backend) checkStationChanges() error {
- // Get current stations from hostapd
- currentStations, err := b.fetchStations()
- if err != nil {
- return err
- }
-
- b.mu.Lock()
- defer b.mu.Unlock()
-
- // Build a map of current MACs
- currentMACs := make(map[string]bool)
- for mac := range currentStations {
- currentMACs[mac] = true
- }
-
- // Check for new stations
- for mac, station := range currentStations {
- if _, exists := b.stations[mac]; !exists {
- // New station connected
- b.stations[mac] = station
- if b.callbacks.OnStationConnected != nil {
- st := b.convertStation(mac, station)
- go b.callbacks.OnStationConnected(st)
- }
- log.Printf("Station connected: %s", mac)
- }
- }
-
- // Check for removed stations
- for mac := range b.stations {
- if !currentMACs[mac] {
- // Station disconnected
- delete(b.stations, mac)
- delete(b.ipByMAC, mac)
- if b.callbacks.OnStationDisconnected != nil {
- go b.callbacks.OnStationDisconnected(mac)
- }
- log.Printf("Station disconnected: %s", mac)
- }
- }
-
- return nil
-}
-
-// loadStations loads the initial list of stations from hostapd
-func (b *Backend) loadStations() error {
- stations, err := b.fetchStations()
- if err != nil {
- return err
- }
-
- b.stations = stations
- log.Printf("Loaded %d initial stations from hostapd", len(b.stations))
- return nil
-}
-
-// fetchStations fetches all stations using hostapd_cli all_sta command
-func (b *Backend) fetchStations() (map[string]*HostapdStation, error) {
- output, err := b.runCommandOutput("all_sta")
- if err != nil {
- return nil, fmt.Errorf("failed to get stations: %w", err)
- }
-
- return b.parseAllStaOutput(output), nil
-}
-
-// parseAllStaOutput parses the output of "hostapd_cli all_sta"
-func (b *Backend) parseAllStaOutput(output string) map[string]*HostapdStation {
- stations := make(map[string]*HostapdStation)
- scanner := bufio.NewScanner(bytes.NewBufferString(output))
-
- var currentMAC string
- var currentStation *HostapdStation
-
- for scanner.Scan() {
- line := strings.TrimSpace(scanner.Text())
- if line == "" {
- continue
- }
-
- // Check if this is a MAC address line (starts the station block)
- if !strings.Contains(line, "=") && len(line) == 17 && strings.Count(line, ":") == 5 {
- // Save previous station if exists
- if currentMAC != "" && currentStation != nil {
- stations[currentMAC] = currentStation
- }
- // Start new station
- currentMAC = strings.ToLower(line)
- currentStation = &HostapdStation{}
- continue
- }
-
- // Parse key=value pairs
- if currentStation != nil && strings.Contains(line, "=") {
- parts := strings.SplitN(line, "=", 2)
- if len(parts) != 2 {
- continue
- }
- key := strings.TrimSpace(parts[0])
- value := strings.TrimSpace(parts[1])
-
- switch key {
- case "signal":
- if v, err := strconv.Atoi(value); err == nil {
- currentStation.Signal = int32(v)
- }
- case "rx_bytes":
- if v, err := strconv.ParseUint(value, 10, 64); err == nil {
- currentStation.RxBytes = v
- }
- case "tx_bytes":
- if v, err := strconv.ParseUint(value, 10, 64); err == nil {
- currentStation.TxBytes = v
- }
- }
- }
- }
-
- // Save last station
- if currentMAC != "" && currentStation != nil {
- stations[currentMAC] = currentStation
- }
-
- return stations
-}
-
-// convertStation converts HostapdStation to backend.Station
-func (b *Backend) convertStation(mac string, hs *HostapdStation) backend.Station {
- // Get IP address if available from correlation
- ip := b.ipByMAC[mac]
-
- // Attempt hostname resolution if we have an IP
- hostname := ""
- // TODO: Could do reverse DNS lookup here if needed
-
- return backend.Station{
- MAC: mac,
- IP: ip,
- Hostname: hostname,
- Type: backend.GuessDeviceType(hostname, mac),
- Signal: hs.Signal,
- RxBytes: hs.RxBytes,
- TxBytes: hs.TxBytes,
- ConnectedAt: time.Now(), // We don't have exact connection time
- }
-}
-
-// UpdateIPMapping updates the MAC -> IP mapping from external source (e.g., DHCP)
-// This should be called periodically to correlate hostapd stations with IP addresses
-func (b *Backend) UpdateIPMapping(macToIP map[string]string) {
- b.mu.Lock()
- defer b.mu.Unlock()
-
- // Track which stations got IP updates
- updated := make(map[string]bool)
-
- for mac, ip := range macToIP {
- if oldIP, exists := b.ipByMAC[mac]; exists && oldIP != ip {
- // IP changed
- updated[mac] = true
- } else if !exists {
- // New IP mapping
- updated[mac] = true
- }
- b.ipByMAC[mac] = ip
- }
-
- // Trigger update callbacks for stations that got new/changed IPs
- for mac := range updated {
- if station, exists := b.stations[mac]; exists {
- if b.callbacks.OnStationUpdated != nil {
- st := b.convertStation(mac, station)
- go b.callbacks.OnStationUpdated(st)
- }
- }
- }
-}
diff --git a/internal/station/hostapd/correlation.go b/internal/station/hostapd/correlation.go
deleted file mode 100644
index fe4361a..0000000
--- a/internal/station/hostapd/correlation.go
+++ /dev/null
@@ -1,130 +0,0 @@
-package hostapd
-
-import (
- "bufio"
- "log"
- "os"
- "strings"
- "time"
-
- "github.com/nemunaire/repeater/internal/models"
-)
-
-// DHCPCorrelator helps correlate hostapd stations with DHCP leases to discover IP addresses
-type DHCPCorrelator struct {
- backend *Backend
- dhcpLeasesPath string
- stopChan chan struct{}
- running bool
-}
-
-// NewDHCPCorrelator creates a new DHCP correlator
-func NewDHCPCorrelator(backend *Backend, dhcpLeasesPath string) *DHCPCorrelator {
- if dhcpLeasesPath == "" {
- dhcpLeasesPath = "/var/lib/dhcp/dhcpd.leases"
- }
-
- return &DHCPCorrelator{
- backend: backend,
- dhcpLeasesPath: dhcpLeasesPath,
- stopChan: make(chan struct{}),
- }
-}
-
-// Start begins periodic correlation of DHCP leases with hostapd stations
-func (dc *DHCPCorrelator) Start() {
- if dc.running {
- return
- }
-
- dc.running = true
- go dc.correlationLoop()
- log.Printf("DHCP correlation started for hostapd backend")
-}
-
-// Stop stops the correlation loop
-func (dc *DHCPCorrelator) Stop() {
- if !dc.running {
- return
- }
-
- dc.running = false
- close(dc.stopChan)
- log.Printf("DHCP correlation stopped")
-}
-
-// correlationLoop periodically correlates DHCP leases with stations
-func (dc *DHCPCorrelator) correlationLoop() {
- // Do an initial correlation immediately
- dc.correlate()
-
- // Then correlate every 10 seconds
- ticker := time.NewTicker(10 * time.Second)
- defer ticker.Stop()
-
- for {
- select {
- case <-ticker.C:
- dc.correlate()
- case <-dc.stopChan:
- return
- }
- }
-}
-
-// correlate performs one correlation cycle
-func (dc *DHCPCorrelator) correlate() {
- // Parse DHCP leases
- leases, err := parseDHCPLeases(dc.dhcpLeasesPath)
- if err != nil {
- log.Printf("Warning: Failed to parse DHCP leases: %v", err)
- return
- }
-
- // Build MAC -> IP mapping
- macToIP := make(map[string]string)
- for _, lease := range leases {
- macToIP[lease.MAC] = lease.IP
- }
-
- // Update backend with IP mappings
- dc.backend.UpdateIPMapping(macToIP)
-}
-
-// parseDHCPLeases reads and parses DHCP lease file
-func parseDHCPLeases(path string) ([]models.DHCPLease, error) {
- var leases []models.DHCPLease
-
- file, err := os.Open(path)
- if err != nil {
- return leases, err
- }
- defer file.Close()
-
- scanner := bufio.NewScanner(file)
- var currentLease models.DHCPLease
-
- for scanner.Scan() {
- line := strings.TrimSpace(scanner.Text())
-
- if strings.HasPrefix(line, "lease ") {
- ip := strings.Fields(line)[1]
- currentLease = models.DHCPLease{IP: ip}
- } else if strings.Contains(line, "hardware ethernet") {
- mac := strings.Fields(line)[2]
- mac = strings.TrimSuffix(mac, ";")
- currentLease.MAC = mac
- } else if strings.Contains(line, "client-hostname") {
- hostname := strings.Fields(line)[1]
- hostname = strings.Trim(hostname, `";`)
- currentLease.Hostname = hostname
- } else if line == "}" {
- if currentLease.IP != "" && currentLease.MAC != "" {
- leases = append(leases, currentLease)
- }
- currentLease = models.DHCPLease{}
- }
- }
-
- return leases, scanner.Err()
-}
diff --git a/internal/station/hostapd/types.go b/internal/station/hostapd/types.go
deleted file mode 100644
index 2521c1f..0000000
--- a/internal/station/hostapd/types.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package hostapd
-
-// HostapdStation represents station properties from hostapd_cli
-type HostapdStation struct {
- RxPackets uint64
- TxPackets uint64
- RxBytes uint64
- TxBytes uint64
- Signal int32 // Signal in dBm
-}
diff --git a/internal/station/station.go b/internal/station/station.go
deleted file mode 100644
index e6acaf1..0000000
--- a/internal/station/station.go
+++ /dev/null
@@ -1,111 +0,0 @@
-package station
-
-import (
- "sync"
-
- "github.com/nemunaire/repeater/internal/models"
- "github.com/nemunaire/repeater/internal/station/backend"
-)
-
-var (
- currentBackend backend.StationBackend
- mu sync.RWMutex
-)
-
-// Initialize initializes the station discovery backend
-func Initialize(backendName string, config backend.BackendConfig) error {
- mu.Lock()
- defer mu.Unlock()
-
- // Close existing backend if any
- if currentBackend != nil {
- currentBackend.Close()
- }
-
- // Create new backend
- b, err := createBackend(backendName)
- if err != nil {
- return err
- }
-
- // Initialize the backend
- if err := b.Initialize(config); err != nil {
- return err
- }
-
- currentBackend = b
- return nil
-}
-
-// GetStations returns all connected stations as ConnectedDevice models
-func GetStations() ([]models.ConnectedDevice, error) {
- mu.RLock()
- defer mu.RUnlock()
-
- if currentBackend == nil {
- return nil, nil
- }
-
- stations, err := currentBackend.GetStations()
- if err != nil {
- return nil, err
- }
-
- // Convert backend.Station to models.ConnectedDevice
- devices := make([]models.ConnectedDevice, len(stations))
- for i, s := range stations {
- devices[i] = models.ConnectedDevice{
- Name: s.Hostname,
- Type: s.Type,
- MAC: s.MAC,
- IP: s.IP,
- }
- }
-
- return devices, nil
-}
-
-// StartEventMonitoring starts monitoring for station events
-func StartEventMonitoring(callbacks backend.EventCallbacks) error {
- mu.RLock()
- defer mu.RUnlock()
-
- if currentBackend == nil {
- return nil
- }
-
- return currentBackend.StartEventMonitoring(callbacks)
-}
-
-// StopEventMonitoring stops monitoring for station events
-func StopEventMonitoring() {
- mu.RLock()
- defer mu.RUnlock()
-
- if currentBackend != nil {
- currentBackend.StopEventMonitoring()
- }
-}
-
-// Close closes the current backend
-func Close() {
- mu.Lock()
- defer mu.Unlock()
-
- if currentBackend != nil {
- currentBackend.Close()
- currentBackend = nil
- }
-}
-
-// SupportsRealTimeEvents returns true if the current backend supports real-time events
-func SupportsRealTimeEvents() bool {
- mu.RLock()
- defer mu.RUnlock()
-
- if currentBackend == nil {
- return false
- }
-
- return currentBackend.SupportsRealTimeEvents()
-}
diff --git a/internal/syslog/parser.go b/internal/syslog/parser.go
deleted file mode 100644
index 10c07b6..0000000
--- a/internal/syslog/parser.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package syslog
-
-import (
- "strings"
-)
-
-// ParseSyslogLine extracts the message content from a syslog line.
-// It looks for the daemon prefix in the line and returns the message after it.
-//
-// Example input: "Dec 2 02:01:33 tyet daemon.info iwd: Error loading /var/lib/iwd//nemuphone.psk"
-// Example output: "Error loading /var/lib/iwd//nemuphone.psk", true
-//
-// Returns the message and a boolean indicating if the line was successfully parsed.
-func ParseSyslogLine(line, daemonPrefix string) (string, bool) {
- // Find the daemon prefix in the line (e.g., "iwd:")
- idx := strings.Index(line, daemonPrefix)
- if idx == -1 {
- return "", false
- }
-
- // Extract everything after the daemon prefix
- message := line[idx+len(daemonPrefix):]
-
- // Trim leading/trailing whitespace
- message = strings.TrimSpace(message)
-
- if message == "" {
- return "", false
- }
-
- return message, true
-}
diff --git a/internal/syslog/syslog.go b/internal/syslog/syslog.go
deleted file mode 100644
index 605a1fe..0000000
--- a/internal/syslog/syslog.go
+++ /dev/null
@@ -1,225 +0,0 @@
-package syslog
-
-import (
- "bufio"
- "io"
- "log"
- "os"
- "strings"
- "sync"
- "time"
-
- "github.com/nemunaire/repeater/internal/logging"
-)
-
-// SyslogTailer tails a syslog file and filters messages to the logging system.
-type SyslogTailer struct {
- path string
- filters []string
- source string
-
- file *os.File
- done chan struct{}
- wg sync.WaitGroup
- mu sync.Mutex
- running bool
-}
-
-// NewSyslogTailer creates a new syslog tailer.
-// path: Path to the syslog file (e.g., "/var/log/messages")
-// filter: Filter string to match in lines (e.g., "daemon.info iwd:")
-// source: Source name for logging (e.g., "iwd")
-func NewSyslogTailer(path string, filters []string, source string) *SyslogTailer {
- return &SyslogTailer{
- path: path,
- filters: filters,
- source: source,
- done: make(chan struct{}),
- }
-}
-
-// Start opens the syslog file and begins tailing it.
-func (t *SyslogTailer) Start() error {
- t.mu.Lock()
- defer t.mu.Unlock()
-
- if t.running {
- return nil
- }
-
- // Try to open the file
- file, err := os.Open(t.path)
- if err != nil {
- // File might not exist yet, we'll retry in the goroutine
- log.Printf("Warning: Cannot open syslog file %s: %v (will retry)", t.path, err)
- } else {
- // Seek to the end to only read new entries
- _, err = file.Seek(0, io.SeekEnd)
- if err != nil {
- file.Close()
- return err
- }
- t.file = file
- }
-
- t.running = true
- t.wg.Add(1)
- go t.tail()
-
- return nil
-}
-
-// Stop signals the tailer to stop and waits for it to finish.
-func (t *SyslogTailer) Stop() {
- t.mu.Lock()
- if !t.running {
- t.mu.Unlock()
- return
- }
- t.mu.Unlock()
-
- close(t.done)
- t.wg.Wait()
-
- t.mu.Lock()
- if t.file != nil {
- t.file.Close()
- t.file = nil
- }
- t.running = false
- t.mu.Unlock()
-}
-
-// tail is the main loop that reads from the syslog file.
-func (t *SyslogTailer) tail() {
- defer t.wg.Done()
-
- retryDelay := 1 * time.Second
- maxRetryDelay := 30 * time.Second
-
- for {
- select {
- case <-t.done:
- return
- default:
- }
-
- // Check if we have a file open
- t.mu.Lock()
- file := t.file
- t.mu.Unlock()
-
- if file == nil {
- // Try to open the file
- newFile, err := os.Open(t.path)
- if err != nil {
- // File doesn't exist or can't be opened, wait and retry
- select {
- case <-t.done:
- return
- case <-time.After(retryDelay):
- // Exponential backoff
- retryDelay *= 2
- if retryDelay > maxRetryDelay {
- retryDelay = maxRetryDelay
- }
- continue
- }
- }
-
- // Seek to the end
- _, err = newFile.Seek(0, io.SeekEnd)
- if err != nil {
- log.Printf("Error seeking syslog file: %v", err)
- newFile.Close()
- time.Sleep(retryDelay)
- continue
- }
-
- t.mu.Lock()
- t.file = newFile
- file = newFile
- t.mu.Unlock()
-
- retryDelay = 1 * time.Second
- log.Printf("Syslog tailer: opened %s", t.path)
- }
-
- // Read lines from the file
- if err := t.readLines(file); err != nil {
- if err == io.EOF {
- // End of file, wait a bit and try again
- time.Sleep(100 * time.Millisecond)
- continue
- }
-
- // Other error, close the file and retry
- log.Printf("Error reading syslog file: %v", err)
- t.mu.Lock()
- if t.file != nil {
- t.file.Close()
- t.file = nil
- }
- t.mu.Unlock()
-
- time.Sleep(retryDelay)
- }
- }
-}
-
-// readLines reads and processes lines from the file.
-func (t *SyslogTailer) readLines(file *os.File) error {
- scanner := bufio.NewScanner(file)
-
- // Increase buffer size to handle long log lines
- const maxCapacity = 512 * 1024
- buf := make([]byte, maxCapacity)
- scanner.Buffer(buf, maxCapacity)
-
- for scanner.Scan() {
- select {
- case <-t.done:
- return nil
- default:
- }
-
- line := scanner.Text()
-
- // Check if the line contains any of the filter strings
- var matchedFilter string
- for _, filter := range t.filters {
- if strings.Contains(line, filter) {
- matchedFilter = filter
- break
- }
- }
- if matchedFilter == "" {
- continue
- }
-
- // Parse the syslog line to extract the message
- // We look for "iwd:" (or whatever comes after the filter)
- // The filter is "daemon.info iwd:" so we want to extract text after "iwd:"
- daemonPrefix := extractDaemonPrefix(matchedFilter)
- message, ok := ParseSyslogLine(line, daemonPrefix)
- if !ok {
- // Couldn't parse the line, skip it
- continue
- }
-
- // Add to logging system
- logging.AddLog(t.source, message)
- }
-
- return scanner.Err()
-}
-
-// extractDaemonPrefix extracts the daemon prefix from the filter string.
-// For example, "daemon.info iwd:" returns "iwd:"
-func extractDaemonPrefix(filter string) string {
- parts := strings.Fields(filter)
- if len(parts) > 0 {
- return parts[len(parts)-1]
- }
- return filter
-}
diff --git a/internal/wifi/backend/types.go b/internal/wifi/backend/types.go
deleted file mode 100644
index 1346e9b..0000000
--- a/internal/wifi/backend/types.go
+++ /dev/null
@@ -1,56 +0,0 @@
-package backend
-
-// WiFiBackend is the interface that must be implemented by all WiFi backends (iwd, wpa_supplicant, etc.)
-type WiFiBackend interface {
- // Lifecycle Management
- Initialize(interfaceName string) error
- Close() error
-
- // Network Discovery
- ScanNetworks() error
- GetOrderedNetworks() ([]BackendNetwork, error)
- IsScanning() (bool, error)
-
- // Connection Management
- Connect(ssid, password string) error
- Disconnect() error
- GetConnectionState() (ConnectionState, error)
- GetConnectedSSID() string
-
- // Event Monitoring
- StartEventMonitoring(callbacks EventCallbacks) error
- StopEventMonitoring()
-}
-
-// BackendNetwork represents a WiFi network in a backend-agnostic format.
-// Both iwd and wpa_supplicant backends convert their native representations to this type.
-type BackendNetwork struct {
- SSID string
- SignalDBm int16 // Signal strength in dBm (-100 to 0)
- SecurityType string // "open", "wep", "psk", "8021x"
- BSSID string // MAC address of the access point
- Frequency uint32 // Frequency in MHz (e.g., 2412 for channel 1, 5180 for channel 36)
-}
-
-// ConnectionState represents the WiFi connection state in a backend-agnostic way.
-type ConnectionState string
-
-const (
- StateConnected ConnectionState = "connected"
- StateDisconnected ConnectionState = "disconnected"
- StateConnecting ConnectionState = "connecting"
- StateDisconnecting ConnectionState = "disconnecting"
-)
-
-// EventCallbacks defines callback functions that backends use to notify the wifi package of events.
-// This allows the wifi package to remain backend-agnostic while still receiving real-time updates.
-type EventCallbacks struct {
- // OnStateChange is called when the connection state changes
- OnStateChange func(state ConnectionState, ssid string)
-
- // OnScanComplete is called when a network scan completes
- OnScanComplete func()
-
- // OnSignalUpdate is called when signal strength changes for the connected network
- OnSignalUpdate func(ssid string, signalDBm int16)
-}
diff --git a/internal/wifi/broadcaster.go b/internal/wifi/broadcaster.go
deleted file mode 100644
index 931ad3e..0000000
--- a/internal/wifi/broadcaster.go
+++ /dev/null
@@ -1,140 +0,0 @@
-package wifi
-
-import (
- "log"
- "sync"
-
- "github.com/gorilla/websocket"
- "github.com/nemunaire/repeater/internal/models"
-)
-
-// WifiBroadcaster manages WebSocket clients and broadcasts WiFi events
-type WifiBroadcaster struct {
- clients map[*websocket.Conn]bool
- clientsMu sync.RWMutex
-
- // State deduplication
- lastState string
- lastSSID string
- lastNetworks []models.WiFiNetwork
- stateMu sync.RWMutex
-}
-
-// NewWifiBroadcaster creates a new WiFi broadcaster
-func NewWifiBroadcaster() *WifiBroadcaster {
- return &WifiBroadcaster{
- clients: make(map[*websocket.Conn]bool),
- }
-}
-
-// RegisterClient registers a new WebSocket client
-func (wb *WifiBroadcaster) RegisterClient(conn *websocket.Conn) {
- wb.clientsMu.Lock()
- wb.clients[conn] = true
- wb.clientsMu.Unlock()
-
- // Send initial state to the new client
- wb.sendInitialState(conn)
-}
-
-// UnregisterClient removes a WebSocket client
-func (wb *WifiBroadcaster) UnregisterClient(conn *websocket.Conn) {
- wb.clientsMu.Lock()
- delete(wb.clients, conn)
- wb.clientsMu.Unlock()
-}
-
-// sendInitialState sends the current WiFi state to a newly connected client
-func (wb *WifiBroadcaster) sendInitialState(conn *websocket.Conn) {
- wb.stateMu.RLock()
- lastState := wb.lastState
- lastSSID := wb.lastSSID
- lastNetworks := make([]models.WiFiNetwork, len(wb.lastNetworks))
- copy(lastNetworks, wb.lastNetworks)
- wb.stateMu.RUnlock()
-
- // Send last known state if available
- if lastState != "" {
- event := NewStateChangeEvent(lastState, lastSSID, "")
- conn.WriteJSON(event)
- }
-
- // Send last known network list if available
- if len(lastNetworks) > 0 {
- event := NewScanUpdateEvent(lastNetworks)
- conn.WriteJSON(event)
- }
-}
-
-// BroadcastScanUpdate broadcasts a scan update event to all clients
-func (wb *WifiBroadcaster) BroadcastScanUpdate(networks []models.WiFiNetwork) {
- // Check for changes to avoid duplicate broadcasts
- wb.stateMu.Lock()
- if networksEqual(wb.lastNetworks, networks) {
- wb.stateMu.Unlock()
- return
- }
- wb.lastNetworks = make([]models.WiFiNetwork, len(networks))
- copy(wb.lastNetworks, networks)
- wb.stateMu.Unlock()
-
- event := NewScanUpdateEvent(networks)
- wb.broadcast(event)
-}
-
-// BroadcastStateChange broadcasts a state change event to all clients
-func (wb *WifiBroadcaster) BroadcastStateChange(state, ssid string) {
- // Check for changes to avoid duplicate broadcasts
- wb.stateMu.Lock()
- if wb.lastState == state && wb.lastSSID == ssid {
- wb.stateMu.Unlock()
- return
- }
- previousState := wb.lastState
- wb.lastState = state
- wb.lastSSID = ssid
- wb.stateMu.Unlock()
-
- event := NewStateChangeEvent(state, ssid, previousState)
- wb.broadcast(event)
-}
-
-// BroadcastSignalUpdate broadcasts a signal update event to all clients
-func (wb *WifiBroadcaster) BroadcastSignalUpdate(ssid string, signal, dbm int) {
- event := NewSignalUpdateEvent(ssid, signal, dbm)
- wb.broadcast(event)
-}
-
-// broadcast sends an event to all connected clients
-func (wb *WifiBroadcaster) broadcast(event WifiEvent) {
- // Get list of clients with read lock
- wb.clientsMu.RLock()
- clients := make([]*websocket.Conn, 0, len(wb.clients))
- for client := range wb.clients {
- clients = append(clients, client)
- }
- wb.clientsMu.RUnlock()
-
- // Broadcast to all clients
- for _, client := range clients {
- err := client.WriteJSON(event)
- if err != nil {
- log.Printf("Erreur lors de l'envoi WebSocket WiFi: %v", err)
- client.Close()
- wb.UnregisterClient(client)
- }
- }
-}
-
-// networksEqual compares two network slices for equality
-func networksEqual(a, b []models.WiFiNetwork) bool {
- if len(a) != len(b) {
- return false
- }
- for i := range a {
- if a[i].SSID != b[i].SSID || a[i].Signal != b[i].Signal {
- return false
- }
- }
- return true
-}
diff --git a/internal/wifi/events.go b/internal/wifi/events.go
deleted file mode 100644
index c8f3672..0000000
--- a/internal/wifi/events.go
+++ /dev/null
@@ -1,70 +0,0 @@
-package wifi
-
-import (
- "time"
-
- "github.com/nemunaire/repeater/internal/models"
-)
-
-// WifiEvent represents a WiFi event to be sent over WebSocket
-type WifiEvent struct {
- Type string `json:"type"`
- Timestamp time.Time `json:"timestamp"`
- Data interface{} `json:"data"`
-}
-
-// ScanUpdateData contains network list update information
-type ScanUpdateData struct {
- Networks []models.WiFiNetwork `json:"networks"`
-}
-
-// StateChangeData contains connection state change information
-type StateChangeData struct {
- State string `json:"state"`
- SSID string `json:"ssid,omitempty"`
- PreviousState string `json:"previous_state,omitempty"`
-}
-
-// SignalUpdateData contains signal strength update information
-type SignalUpdateData struct {
- SSID string `json:"ssid"`
- Signal int `json:"signal"` // 1-5 scale
- DBm int `json:"dbm"` // Raw dBm value
-}
-
-// NewScanUpdateEvent creates a new scan update event
-func NewScanUpdateEvent(networks []models.WiFiNetwork) WifiEvent {
- return WifiEvent{
- Type: "scan_update",
- Timestamp: time.Now(),
- Data: ScanUpdateData{
- Networks: networks,
- },
- }
-}
-
-// NewStateChangeEvent creates a new state change event
-func NewStateChangeEvent(state, ssid, previousState string) WifiEvent {
- return WifiEvent{
- Type: "state_change",
- Timestamp: time.Now(),
- Data: StateChangeData{
- State: state,
- SSID: ssid,
- PreviousState: previousState,
- },
- }
-}
-
-// NewSignalUpdateEvent creates a new signal update event
-func NewSignalUpdateEvent(ssid string, signal, dbm int) WifiEvent {
- return WifiEvent{
- Type: "signal_update",
- Timestamp: time.Now(),
- Data: SignalUpdateData{
- SSID: ssid,
- Signal: signal,
- DBm: dbm,
- },
- }
-}
diff --git a/internal/wifi/factory.go b/internal/wifi/factory.go
deleted file mode 100644
index fdb63cc..0000000
--- a/internal/wifi/factory.go
+++ /dev/null
@@ -1,21 +0,0 @@
-package wifi
-
-import (
- "fmt"
-
- "github.com/nemunaire/repeater/internal/wifi/backend"
- "github.com/nemunaire/repeater/internal/wifi/iwd"
- "github.com/nemunaire/repeater/internal/wifi/wpasupplicant"
-)
-
-// createBackend creates the appropriate WiFi backend based on the backend name
-func createBackend(backendName string) (backend.WiFiBackend, error) {
- switch backendName {
- case "iwd":
- return iwd.NewIWDBackend(), nil
- case "wpasupplicant":
- return wpasupplicant.NewWPABackend(), nil
- default:
- return nil, fmt.Errorf("invalid wifi backend: %s (must be 'iwd' or 'wpasupplicant')", backendName)
- }
-}
diff --git a/internal/wifi/iwd/agent.go b/internal/wifi/iwd/agent.go
deleted file mode 100644
index 3a53ba6..0000000
--- a/internal/wifi/iwd/agent.go
+++ /dev/null
@@ -1,170 +0,0 @@
-package iwd
-
-import (
- "fmt"
- "sync"
-
- "github.com/godbus/dbus/v5"
- "github.com/godbus/dbus/v5/introspect"
-)
-
-const agentIntrospectXML = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`
-
-// Agent implements the net.connman.iwd.Agent interface for credential callbacks
-type Agent struct {
- conn *dbus.Conn
- path dbus.ObjectPath
- passphraseStore map[string]string
- mu sync.RWMutex
-}
-
-// NewAgent creates a new Agent instance
-func NewAgent(conn *dbus.Conn, path dbus.ObjectPath) *Agent {
- return &Agent{
- conn: conn,
- path: path,
- passphraseStore: make(map[string]string),
- }
-}
-
-// SetPassphrase stores a passphrase for a given network SSID
-func (a *Agent) SetPassphrase(ssid, passphrase string) {
- a.mu.Lock()
- defer a.mu.Unlock()
- a.passphraseStore[ssid] = passphrase
-}
-
-// ClearPassphrase removes the stored passphrase for a network
-func (a *Agent) ClearPassphrase(ssid string) {
- a.mu.Lock()
- defer a.mu.Unlock()
- delete(a.passphraseStore, ssid)
-}
-
-// Export registers the agent object on D-Bus
-func (a *Agent) Export() error {
- err := a.conn.Export(a, a.path, AgentInterface)
- if err != nil {
- return fmt.Errorf("failed to export agent: %v", err)
- }
-
- err = a.conn.Export(introspect.Introspectable(agentIntrospectXML), a.path, "org.freedesktop.DBus.Introspectable")
- if err != nil {
- a.conn.Export(nil, a.path, AgentInterface)
- return fmt.Errorf("failed to export introspection: %v", err)
- }
-
- return nil
-}
-
-// Unexport unregisters the agent from D-Bus
-func (a *Agent) Unexport() {
- a.conn.Export(nil, a.path, AgentInterface)
- a.conn.Export(nil, a.path, "org.freedesktop.DBus.Introspectable")
-}
-
-// getNetworkSSID queries the network object to get its SSID (Name property)
-func (a *Agent) getNetworkSSID(networkPath dbus.ObjectPath) (string, error) {
- obj := a.conn.Object(Service, networkPath)
- variant, err := obj.GetProperty(NetworkInterface + ".Name")
- if err != nil {
- return "", fmt.Errorf("failed to get network name: %v", err)
- }
-
- name, ok := variant.Value().(string)
- if !ok {
- return "", fmt.Errorf("network name is not a string")
- }
-
- return name, nil
-}
-
-// RequestPassphrase is called by iwd when connecting to PSK networks
-func (a *Agent) RequestPassphrase(network dbus.ObjectPath) (string, *dbus.Error) {
- fmt.Printf("[Agent] RequestPassphrase called for network: %s\n", network)
-
- ssid, err := a.getNetworkSSID(network)
- if err != nil {
- fmt.Printf("[Agent] Failed to get SSID: %v\n", err)
- return "", dbus.MakeFailedError(fmt.Errorf("failed to get network SSID: %v", err))
- }
-
- fmt.Printf("[Agent] Network SSID: %s\n", ssid)
-
- a.mu.RLock()
- passphrase, ok := a.passphraseStore[ssid]
- a.mu.RUnlock()
-
- if !ok {
- fmt.Printf("[Agent] No passphrase stored for SSID: %s\n", ssid)
- return "", dbus.MakeFailedError(fmt.Errorf("no passphrase stored for network '%s'", ssid))
- }
-
- fmt.Printf("[Agent] Returning passphrase for SSID: %s\n", ssid)
- return passphrase, nil
-}
-
-// RequestPrivateKeyPassphrase is called for encrypted private keys
-func (a *Agent) RequestPrivateKeyPassphrase(network dbus.ObjectPath) (string, *dbus.Error) {
- // Not implemented for now
- return "", dbus.MakeFailedError(fmt.Errorf("RequestPrivateKeyPassphrase not implemented"))
-}
-
-// RequestUserNameAndPassword is called for enterprise networks
-func (a *Agent) RequestUserNameAndPassword(network dbus.ObjectPath) (string, string, *dbus.Error) {
- // Not implemented for now
- return "", "", dbus.MakeFailedError(fmt.Errorf("RequestUserNameAndPassword not implemented"))
-}
-
-// RequestUserPassword is called for enterprise networks with known username
-func (a *Agent) RequestUserPassword(network dbus.ObjectPath, user string) (string, *dbus.Error) {
- // Not implemented for now
- return "", dbus.MakeFailedError(fmt.Errorf("RequestUserPassword not implemented"))
-}
-
-// Cancel is called when a request is canceled
-func (a *Agent) Cancel(reason string) *dbus.Error {
- // Nothing to do, just acknowledge
- return nil
-}
-
-// Release is called when the agent is unregistered
-func (a *Agent) Release() *dbus.Error {
- // Cleanup if needed
- a.mu.Lock()
- a.passphraseStore = make(map[string]string)
- a.mu.Unlock()
- return nil
-}
diff --git a/internal/wifi/iwd/agentmanager.go b/internal/wifi/iwd/agentmanager.go
deleted file mode 100644
index 7ea6c77..0000000
--- a/internal/wifi/iwd/agentmanager.go
+++ /dev/null
@@ -1,39 +0,0 @@
-package iwd
-
-import (
- "fmt"
-
- "github.com/godbus/dbus/v5"
-)
-
-// AgentManager handles agent registration with iwd
-type AgentManager struct {
- conn *dbus.Conn
- obj dbus.BusObject
-}
-
-// NewAgentManager creates a new AgentManager instance
-func NewAgentManager(conn *dbus.Conn) *AgentManager {
- return &AgentManager{
- conn: conn,
- obj: conn.Object(Service, "/net/connman/iwd"),
- }
-}
-
-// RegisterAgent registers an agent with iwd
-func (am *AgentManager) RegisterAgent(agentPath dbus.ObjectPath) error {
- err := am.obj.Call(AgentManagerInterface+".RegisterAgent", 0, agentPath).Err
- if err != nil {
- return fmt.Errorf("failed to register agent: %v", err)
- }
- return nil
-}
-
-// UnregisterAgent unregisters an agent from iwd
-func (am *AgentManager) UnregisterAgent(agentPath dbus.ObjectPath) error {
- err := am.obj.Call(AgentManagerInterface+".UnregisterAgent", 0, agentPath).Err
- if err != nil {
- return fmt.Errorf("failed to unregister agent: %v", err)
- }
- return nil
-}
diff --git a/internal/wifi/iwd/backend.go b/internal/wifi/iwd/backend.go
deleted file mode 100644
index 615a280..0000000
--- a/internal/wifi/iwd/backend.go
+++ /dev/null
@@ -1,255 +0,0 @@
-package iwd
-
-import (
- "fmt"
-
- "github.com/godbus/dbus/v5"
- "github.com/nemunaire/repeater/internal/wifi/backend"
-)
-
-const (
- AgentPath = "/com/github/nemunaire/repeater/agent"
-)
-
-// IWDBackend implements the WiFiBackend interface for iwd (Intel Wireless Daemon)
-type IWDBackend struct {
- conn *dbus.Conn
- manager *Manager
- station *Station
- agent *Agent
- agentManager *AgentManager
- signalMonitor *SignalMonitor
- interfaceName string
- callbacks backend.EventCallbacks
-}
-
-// NewIWDBackend creates a new IWD backend instance
-func NewIWDBackend() *IWDBackend {
- return &IWDBackend{}
-}
-
-// Initialize initializes the iwd backend with the given interface name
-func (b *IWDBackend) Initialize(interfaceName string) error {
- b.interfaceName = interfaceName
- var err error
-
- // Connect to D-Bus
- b.conn, err = dbus.SystemBus()
- if err != nil {
- return fmt.Errorf("échec de connexion à D-Bus: %v", err)
- }
-
- // Find station for interface
- b.manager = NewManager(b.conn)
- b.station, err = b.manager.FindStation(interfaceName)
- if err != nil {
- return fmt.Errorf("impossible de trouver la station pour %s: %v", interfaceName, err)
- }
-
- // Create and register agent for credential callbacks
- b.agent = NewAgent(b.conn, dbus.ObjectPath(AgentPath))
- if err := b.agent.Export(); err != nil {
- return fmt.Errorf("échec de l'export de l'agent: %v", err)
- }
-
- b.agentManager = NewAgentManager(b.conn)
- if err := b.agentManager.RegisterAgent(dbus.ObjectPath(AgentPath)); err != nil {
- b.agent.Unexport()
- return fmt.Errorf("échec de l'enregistrement de l'agent: %v", err)
- }
-
- return nil
-}
-
-// Close closes the D-Bus connection and unregisters the agent
-func (b *IWDBackend) Close() error {
- if b.agentManager != nil && b.agent != nil {
- b.agentManager.UnregisterAgent(dbus.ObjectPath(AgentPath))
- b.agent.Unexport()
- }
- if b.conn != nil {
- b.conn.Close()
- }
- return nil
-}
-
-// ScanNetworks triggers a network scan
-func (b *IWDBackend) ScanNetworks() error {
- err := b.station.Scan()
- if err != nil {
- return fmt.Errorf("erreur lors du scan: %v", err)
- }
- return nil
-}
-
-// GetOrderedNetworks returns networks sorted by signal strength in backend-agnostic format
-func (b *IWDBackend) GetOrderedNetworks() ([]backend.BackendNetwork, error) {
- networkInfos, err := b.station.GetOrderedNetworks()
- if err != nil {
- return nil, fmt.Errorf("erreur lors de la récupération des réseaux: %v", err)
- }
-
- var networks []backend.BackendNetwork
- seenSSIDs := make(map[string]bool)
-
- for _, netInfo := range networkInfos {
- network := NewNetwork(b.conn, netInfo.Path)
- props, err := network.GetProperties()
- if err != nil {
- continue
- }
-
- if props.Name == "" || seenSSIDs[props.Name] {
- continue
- }
- seenSSIDs[props.Name] = true
-
- // Convert iwd network to backend-agnostic format
- backendNet := backend.BackendNetwork{
- SSID: props.Name,
- SignalDBm: netInfo.Signal / 100, // iwd provides 100*dBm, convert to dBm
- SecurityType: props.Type,
- BSSID: generateSyntheticBSSID(props.Name), // iwd doesn't expose BSSID
- Frequency: 0, // iwd doesn't expose frequency in GetOrderedNetworks
- }
-
- networks = append(networks, backendNet)
- }
-
- return networks, nil
-}
-
-// IsScanning checks if a scan is currently in progress
-func (b *IWDBackend) IsScanning() (bool, error) {
- return b.station.IsScanning()
-}
-
-// Connect connects to a WiFi network
-func (b *IWDBackend) Connect(ssid, password string) error {
- // Store passphrase in agent for callback
- if password != "" {
- b.agent.SetPassphrase(ssid, password)
- }
-
- // Ensure passphrase is cleared after connection attempt
- defer func() {
- if password != "" {
- b.agent.ClearPassphrase(ssid)
- }
- }()
-
- // Get network object
- network, err := b.station.GetNetwork(ssid)
- if err != nil {
- return fmt.Errorf("réseau '%s' non trouvé: %v", ssid, err)
- }
-
- // Connect - iwd will call agent.RequestPassphrase() if needed
- if err := network.Connect(); err != nil {
- return fmt.Errorf("erreur lors de la connexion: %v", err)
- }
-
- return nil
-}
-
-// Disconnect disconnects from the current WiFi network
-func (b *IWDBackend) Disconnect() error {
- if err := b.station.Disconnect(); err != nil {
- return fmt.Errorf("erreur lors de la déconnexion: %v", err)
- }
- return nil
-}
-
-// GetConnectionState returns the current WiFi connection state
-func (b *IWDBackend) GetConnectionState() (backend.ConnectionState, error) {
- state, err := b.station.GetState()
- if err != nil {
- return backend.StateDisconnected, err
- }
- return mapIWDState(state), nil
-}
-
-// GetConnectedSSID returns the SSID of the currently connected network
-func (b *IWDBackend) GetConnectedSSID() string {
- network, err := b.station.GetConnectedNetwork()
- if err != nil {
- return ""
- }
-
- props, err := network.GetProperties()
- if err != nil {
- return ""
- }
-
- return props.Name
-}
-
-// StartEventMonitoring starts monitoring WiFi events
-func (b *IWDBackend) StartEventMonitoring(callbacks backend.EventCallbacks) error {
- b.callbacks = callbacks
-
- // Create signal monitor
- b.signalMonitor = NewSignalMonitor(b.conn, b.station)
-
- // Register callbacks - wrap to convert iwd types to backend types
- b.signalMonitor.OnStateChange(func(state StationState, ssid string) {
- if b.callbacks.OnStateChange != nil {
- b.callbacks.OnStateChange(mapIWDState(state), ssid)
- }
- })
-
- b.signalMonitor.OnScanComplete(func() {
- if b.callbacks.OnScanComplete != nil {
- b.callbacks.OnScanComplete()
- }
- })
-
- // Start monitoring
- return b.signalMonitor.Start()
-}
-
-// StopEventMonitoring stops monitoring WiFi events
-func (b *IWDBackend) StopEventMonitoring() {
- if b.signalMonitor != nil {
- b.signalMonitor.Stop()
- }
-}
-
-// mapIWDState maps iwd-specific states to backend-agnostic states
-func mapIWDState(state StationState) backend.ConnectionState {
- switch state {
- case StateConnected:
- return backend.StateConnected
- case StateConnecting:
- return backend.StateConnecting
- case StateDisconnecting:
- return backend.StateDisconnecting
- case StateDisconnected:
- return backend.StateDisconnected
- case StateRoaming:
- // Map roaming to connected since we're still connected during roaming
- return backend.StateConnected
- default:
- return backend.StateDisconnected
- }
-}
-
-// generateSyntheticBSSID generates a consistent fake BSSID from SSID
-// (iwd doesn't expose real BSSID)
-func generateSyntheticBSSID(ssid string) string {
- // Use a simple hash approach - consistent per SSID
- hash := 0
- for _, c := range ssid {
- hash = ((hash << 5) - hash) + int(c)
- }
-
- // Generate 6 bytes for MAC address
- b1 := byte((hash >> 0) & 0xff)
- b2 := byte((hash >> 8) & 0xff)
- b3 := byte((hash >> 16) & 0xff)
- b4 := byte((hash >> 24) & 0xff)
- b5 := byte(len(ssid) & 0xff)
- b6 := byte((len(ssid) >> 8) & 0xff)
-
- return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", b1, b2, b3, b4, b5, b6)
-}
diff --git a/internal/wifi/iwd/manager.go b/internal/wifi/iwd/manager.go
deleted file mode 100644
index f636664..0000000
--- a/internal/wifi/iwd/manager.go
+++ /dev/null
@@ -1,71 +0,0 @@
-package iwd
-
-import (
- "fmt"
- "strings"
-
- "github.com/godbus/dbus/v5"
-)
-
-// Manager handles iwd object discovery via ObjectManager
-type Manager struct {
- conn *dbus.Conn
- obj dbus.BusObject
-}
-
-// NewManager creates a new Manager instance
-func NewManager(conn *dbus.Conn) *Manager {
- return &Manager{
- conn: conn,
- obj: conn.Object(Service, dbus.ObjectPath(ManagerPath)),
- }
-}
-
-// GetManagedObjects returns all iwd managed objects
-func (m *Manager) GetManagedObjects() (map[dbus.ObjectPath]map[string]map[string]dbus.Variant, error) {
- var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant
- err := m.obj.Call("org.freedesktop.DBus.ObjectManager.GetManagedObjects", 0).Store(&objects)
- if err != nil {
- return nil, fmt.Errorf("failed to get managed objects: %v", err)
- }
- return objects, nil
-}
-
-// FindStation finds the Station object for the given interface name
-func (m *Manager) FindStation(interfaceName string) (*Station, error) {
- objects, err := m.GetManagedObjects()
- if err != nil {
- return nil, err
- }
-
- // First, find the device with matching interface name
- var devicePath dbus.ObjectPath
- for path, interfaces := range objects {
- if deviceProps, ok := interfaces[DeviceInterface]; ok {
- if nameVariant, ok := deviceProps["Name"]; ok {
- if name, ok := nameVariant.Value().(string); ok && name == interfaceName {
- devicePath = path
- break
- }
- }
- }
- }
-
- if devicePath == "" {
- return nil, fmt.Errorf("device with interface '%s' not found", interfaceName)
- }
-
- // Now find the station object under this device
- // Station path is typically the same as device path or a child of it
- for path, interfaces := range objects {
- if _, ok := interfaces[StationInterface]; ok {
- // Check if this station belongs to our device
- // Station path should be the device path or start with it
- if path == devicePath || strings.HasPrefix(string(path), string(devicePath)+"/") {
- return NewStation(m.conn, path), nil
- }
- }
- }
-
- return nil, fmt.Errorf("station for device '%s' not found", interfaceName)
-}
diff --git a/internal/wifi/iwd/network.go b/internal/wifi/iwd/network.go
deleted file mode 100644
index 1563a95..0000000
--- a/internal/wifi/iwd/network.go
+++ /dev/null
@@ -1,64 +0,0 @@
-package iwd
-
-import (
- "fmt"
-
- "github.com/godbus/dbus/v5"
-)
-
-// Network represents an iwd Network interface
-type Network struct {
- path dbus.ObjectPath
- conn *dbus.Conn
- obj dbus.BusObject
-}
-
-// NewNetwork creates a new Network instance
-func NewNetwork(conn *dbus.Conn, path dbus.ObjectPath) *Network {
- return &Network{
- path: path,
- conn: conn,
- obj: conn.Object(Service, path),
- }
-}
-
-// GetProperties retrieves all network properties
-func (n *Network) GetProperties() (*NetworkProperties, error) {
- var props map[string]dbus.Variant
- err := n.obj.Call("org.freedesktop.DBus.Properties.GetAll", 0, NetworkInterface).Store(&props)
- if err != nil {
- return nil, fmt.Errorf("failed to get network properties: %v", err)
- }
-
- netProps := &NetworkProperties{}
-
- if nameVariant, ok := props["Name"]; ok {
- if name, ok := nameVariant.Value().(string); ok {
- netProps.Name = name
- }
- }
-
- if typeVariant, ok := props["Type"]; ok {
- if netType, ok := typeVariant.Value().(string); ok {
- netProps.Type = netType
- }
- }
-
- if connectedVariant, ok := props["Connected"]; ok {
- if connected, ok := connectedVariant.Value().(bool); ok {
- netProps.Connected = connected
- }
- }
-
- return netProps, nil
-}
-
-// Connect initiates a connection to this network
-// Credentials are provided via the registered agent's RequestPassphrase callback
-func (n *Network) Connect() error {
- err := n.obj.Call(NetworkInterface+".Connect", 0).Err
- if err != nil {
- return fmt.Errorf("connect failed: %v", err)
- }
- return nil
-}
diff --git a/internal/wifi/iwd/signals.go b/internal/wifi/iwd/signals.go
deleted file mode 100644
index 90ed32e..0000000
--- a/internal/wifi/iwd/signals.go
+++ /dev/null
@@ -1,223 +0,0 @@
-package iwd
-
-import (
- "log"
- "sync"
-
- "github.com/godbus/dbus/v5"
-)
-
-// SignalMonitor monitors D-Bus signals from iwd
-type SignalMonitor struct {
- conn *dbus.Conn
- station *Station
-
- // Signal channel
- signalChan chan *dbus.Signal
-
- // Callbacks
- onStateChange func(state StationState, ssid string)
- onScanComplete func()
-
- // Control
- stopChan chan struct{}
- mu sync.RWMutex
- running bool
-
- // State tracking
- lastScanning bool
-}
-
-// NewSignalMonitor creates a new signal monitor
-func NewSignalMonitor(conn *dbus.Conn, station *Station) *SignalMonitor {
- return &SignalMonitor{
- conn: conn,
- station: station,
- signalChan: make(chan *dbus.Signal, 100),
- stopChan: make(chan struct{}),
- }
-}
-
-// OnStateChange registers a callback for state changes
-func (sm *SignalMonitor) OnStateChange(callback func(state StationState, ssid string)) {
- sm.mu.Lock()
- defer sm.mu.Unlock()
- sm.onStateChange = callback
-}
-
-// OnScanComplete registers a callback for scan completion
-func (sm *SignalMonitor) OnScanComplete(callback func()) {
- sm.mu.Lock()
- defer sm.mu.Unlock()
- sm.onScanComplete = callback
-}
-
-// Start begins monitoring D-Bus signals
-func (sm *SignalMonitor) Start() error {
- sm.mu.Lock()
- if sm.running {
- sm.mu.Unlock()
- return nil
- }
- sm.running = true
- sm.mu.Unlock()
-
- // Subscribe to PropertiesChanged signals for Station interface
- stationPath := sm.station.GetPath()
-
- // Add signal match for PropertiesChanged on Station interface
- matchOptions := []dbus.MatchOption{
- dbus.WithMatchObjectPath(stationPath),
- dbus.WithMatchInterface("org.freedesktop.DBus.Properties"),
- dbus.WithMatchMember("PropertiesChanged"),
- }
-
- if err := sm.conn.AddMatchSignal(matchOptions...); err != nil {
- sm.mu.Lock()
- sm.running = false
- sm.mu.Unlock()
- return err
- }
-
- // Register signal channel
- sm.conn.Signal(sm.signalChan)
-
- // Get initial scanning state
- scanning, err := sm.station.IsScanning()
- if err == nil {
- sm.lastScanning = scanning
- }
-
- // Start monitoring goroutine
- go sm.monitor()
-
- log.Printf("D-Bus signal monitoring started for station %s", stationPath)
- return nil
-}
-
-// Stop stops monitoring D-Bus signals
-func (sm *SignalMonitor) Stop() {
- sm.mu.Lock()
- if !sm.running {
- sm.mu.Unlock()
- return
- }
- sm.running = false
- sm.mu.Unlock()
-
- // Signal stop
- close(sm.stopChan)
-
- // Remove signal channel
- sm.conn.RemoveSignal(sm.signalChan)
-
- log.Printf("D-Bus signal monitoring stopped")
-}
-
-// monitor is the main signal processing loop
-func (sm *SignalMonitor) monitor() {
- for {
- select {
- case sig := <-sm.signalChan:
- sm.handleSignal(sig)
- case <-sm.stopChan:
- return
- }
- }
-}
-
-// handleSignal processes a D-Bus signal
-func (sm *SignalMonitor) handleSignal(sig *dbus.Signal) {
- // Only process PropertiesChanged signals
- if sig.Name != "org.freedesktop.DBus.Properties.PropertiesChanged" {
- return
- }
-
- // Verify signal is from Station interface
- if len(sig.Body) < 2 {
- return
- }
-
- interfaceName, ok := sig.Body[0].(string)
- if !ok || interfaceName != StationInterface {
- return
- }
-
- // Parse changed properties
- changedProps, ok := sig.Body[1].(map[string]dbus.Variant)
- if !ok {
- return
- }
-
- // Check for State property change
- if stateVariant, ok := changedProps["State"]; ok {
- if state, ok := stateVariant.Value().(string); ok {
- sm.handleStateChange(StationState(state))
- }
- }
-
- // Check for Scanning property change
- if scanningVariant, ok := changedProps["Scanning"]; ok {
- if scanning, ok := scanningVariant.Value().(bool); ok {
- sm.handleScanningChange(scanning)
- }
- }
-
- // Check for ConnectedNetwork property change
- if _, ok := changedProps["ConnectedNetwork"]; ok {
- // Network connection changed, trigger state update
- sm.handleConnectionChange()
- }
-}
-
-// handleStateChange processes a state change
-func (sm *SignalMonitor) handleStateChange(state StationState) {
- sm.mu.RLock()
- callback := sm.onStateChange
- sm.mu.RUnlock()
-
- if callback == nil {
- return
- }
-
- // Get connected SSID if connected
- ssid := ""
- if state == StateConnected {
- network, err := sm.station.GetConnectedNetwork()
- if err == nil {
- props, err := network.GetProperties()
- if err == nil {
- ssid = props.Name
- }
- }
- }
-
- callback(state, ssid)
-}
-
-// handleScanningChange processes scanning state changes
-func (sm *SignalMonitor) handleScanningChange(scanning bool) {
- // Detect scan completion (transition from true to false)
- if sm.lastScanning && !scanning {
- sm.mu.RLock()
- callback := sm.onScanComplete
- sm.mu.RUnlock()
-
- if callback != nil {
- callback()
- }
- }
-
- sm.lastScanning = scanning
-}
-
-// handleConnectionChange processes connection changes
-func (sm *SignalMonitor) handleConnectionChange() {
- // Get current state and trigger state change callback
- state, err := sm.station.GetState()
- if err != nil {
- return
- }
-
- sm.handleStateChange(state)
-}
diff --git a/internal/wifi/iwd/station.go b/internal/wifi/iwd/station.go
deleted file mode 100644
index 54014fd..0000000
--- a/internal/wifi/iwd/station.go
+++ /dev/null
@@ -1,142 +0,0 @@
-package iwd
-
-import (
- "fmt"
-
- "github.com/godbus/dbus/v5"
-)
-
-// Station represents an iwd Station interface
-type Station struct {
- path dbus.ObjectPath
- conn *dbus.Conn
- obj dbus.BusObject
-}
-
-// NewStation creates a new Station instance
-func NewStation(conn *dbus.Conn, path dbus.ObjectPath) *Station {
- return &Station{
- path: path,
- conn: conn,
- obj: conn.Object(Service, path),
- }
-}
-
-// Scan triggers a network scan
-func (s *Station) Scan() error {
- err := s.obj.Call(StationInterface+".Scan", 0).Err
- if err != nil {
- return fmt.Errorf("scan failed: %v", err)
- }
- return nil
-}
-
-// IsScanning checks if a scan is currently in progress
-func (s *Station) IsScanning() (bool, error) {
- prop, err := s.obj.GetProperty(StationInterface + ".Scanning")
- if err != nil {
- return false, fmt.Errorf("failed to get Scanning property: %v", err)
- }
-
- scanning, ok := prop.Value().(bool)
- if !ok {
- return false, fmt.Errorf("Scanning property is not a boolean")
- }
-
- return scanning, nil
-}
-
-// GetOrderedNetworks returns networks sorted by signal strength
-func (s *Station) GetOrderedNetworks() ([]NetworkInfo, error) {
- var result []struct {
- Path dbus.ObjectPath
- Signal int16
- }
-
- err := s.obj.Call(StationInterface+".GetOrderedNetworks", 0).Store(&result)
- if err != nil {
- return nil, fmt.Errorf("failed to get ordered networks: %v", err)
- }
-
- networks := make([]NetworkInfo, len(result))
- for i, r := range result {
- networks[i] = NetworkInfo{
- Path: r.Path,
- Signal: r.Signal,
- }
- }
-
- return networks, nil
-}
-
-// GetState returns the current connection state
-func (s *Station) GetState() (StationState, error) {
- prop, err := s.obj.GetProperty(StationInterface + ".State")
- if err != nil {
- return "", fmt.Errorf("failed to get State property: %v", err)
- }
-
- state, ok := prop.Value().(string)
- if !ok {
- return "", fmt.Errorf("State property is not a string")
- }
-
- return StationState(state), nil
-}
-
-// Disconnect disconnects from the current network
-func (s *Station) Disconnect() error {
- err := s.obj.Call(StationInterface+".Disconnect", 0).Err
- if err != nil {
- return fmt.Errorf("disconnect failed: %v", err)
- }
- return nil
-}
-
-// GetNetwork finds and returns a Network object by SSID
-func (s *Station) GetNetwork(ssid string) (*Network, error) {
- networks, err := s.GetOrderedNetworks()
- if err != nil {
- return nil, err
- }
-
- // Find the network with matching SSID
- for _, netInfo := range networks {
- network := NewNetwork(s.conn, netInfo.Path)
- props, err := network.GetProperties()
- if err != nil {
- continue
- }
-
- if props.Name == ssid {
- return network, nil
- }
- }
-
- return nil, fmt.Errorf("network '%s' not found", ssid)
-}
-
-// GetConnectedNetwork returns the currently connected network
-func (s *Station) GetConnectedNetwork() (*Network, error) {
- prop, err := s.obj.GetProperty(StationInterface + ".ConnectedNetwork")
- if err != nil {
- return nil, fmt.Errorf("failed to get ConnectedNetwork property: %v", err)
- }
-
- path, ok := prop.Value().(dbus.ObjectPath)
- if !ok {
- return nil, fmt.Errorf("ConnectedNetwork property is not an ObjectPath")
- }
-
- // Check if path is empty (not connected)
- if path == "/" || path == "" {
- return nil, fmt.Errorf("not connected to any network")
- }
-
- return NewNetwork(s.conn, path), nil
-}
-
-// GetPath returns the D-Bus object path for this station
-func (s *Station) GetPath() dbus.ObjectPath {
- return s.path
-}
diff --git a/internal/wifi/iwd/types.go b/internal/wifi/iwd/types.go
deleted file mode 100644
index 76624c4..0000000
--- a/internal/wifi/iwd/types.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package iwd
-
-import "github.com/godbus/dbus/v5"
-
-const (
- // D-Bus service and interfaces
- Service = "net.connman.iwd"
- ManagerPath = "/"
- DeviceInterface = "net.connman.iwd.Device"
- StationInterface = "net.connman.iwd.Station"
- NetworkInterface = "net.connman.iwd.Network"
- AgentInterface = "net.connman.iwd.Agent"
- AgentManagerInterface = "net.connman.iwd.AgentManager"
-)
-
-// NetworkInfo represents a network with its signal strength
-type NetworkInfo struct {
- Path dbus.ObjectPath
- Signal int16 // 100 * dBm (0 to -10000)
-}
-
-// NetworkProperties holds network properties
-type NetworkProperties struct {
- Name string // SSID
- Type string // "open", "wep", "psk", "8021x"
- Connected bool
-}
-
-// StationState represents the connection state
-type StationState string
-
-const (
- StateConnected StationState = "connected"
- StateDisconnected StationState = "disconnected"
- StateConnecting StationState = "connecting"
- StateDisconnecting StationState = "disconnecting"
- StateRoaming StationState = "roaming"
-)
diff --git a/internal/wifi/wifi.go b/internal/wifi/wifi.go
deleted file mode 100644
index dac04c3..0000000
--- a/internal/wifi/wifi.go
+++ /dev/null
@@ -1,242 +0,0 @@
-package wifi
-
-import (
- "fmt"
- "sort"
- "strings"
- "time"
-
- "github.com/gorilla/websocket"
- "github.com/nemunaire/repeater/internal/models"
- "github.com/nemunaire/repeater/internal/wifi/backend"
-)
-
-var (
- wifiBackend backend.WiFiBackend
- wifiBroadcaster *WifiBroadcaster
-)
-
-// Initialize initializes the WiFi service with the specified backend
-func Initialize(interfaceName string, backendName string) error {
- // Create the appropriate backend using the factory
- var err error
- wifiBackend, err = createBackend(backendName)
- if err != nil {
- return err
- }
-
- // Initialize the backend
- return wifiBackend.Initialize(interfaceName)
-}
-
-// Close closes the backend connection
-func Close() {
- if wifiBackend != nil {
- wifiBackend.Close()
- }
-}
-
-// GetCachedNetworks returns previously discovered networks without triggering a scan
-func GetCachedNetworks() ([]models.WiFiNetwork, error) {
- // Get ordered networks from backend
- backendNetworks, err := wifiBackend.GetOrderedNetworks()
- if err != nil {
- return nil, fmt.Errorf("erreur lors de la récupération des réseaux: %v", err)
- }
-
- // Convert backend networks to models
- networks := make([]models.WiFiNetwork, 0, len(backendNetworks))
- for _, backendNet := range backendNetworks {
- wifiNet := models.WiFiNetwork{
- SSID: backendNet.SSID,
- Signal: signalToStrength(int(backendNet.SignalDBm)),
- Security: mapSecurityType(backendNet.SecurityType),
- BSSID: backendNet.BSSID,
- Channel: 0, // Not yet exposed by backends
- }
- networks = append(networks, wifiNet)
- }
-
- // Sort by signal strength (descending)
- sort.Slice(networks, func(i, j int) bool {
- return networks[i].Signal > networks[j].Signal
- })
-
- return networks, nil
-}
-
-// ScanNetworks scans for available WiFi networks
-func ScanNetworks() ([]models.WiFiNetwork, error) {
- // Check if already scanning
- scanning, err := wifiBackend.IsScanning()
- if err == nil && scanning {
- time.Sleep(3 * time.Second)
- } else {
- // Trigger scan
- err := wifiBackend.ScanNetworks()
- if err != nil && !strings.Contains(err.Error(), "rejected") {
- return nil, fmt.Errorf("erreur lors du scan: %v", err)
- }
- time.Sleep(2 * time.Second)
- }
-
- // Get ordered networks from backend
- backendNetworks, err := wifiBackend.GetOrderedNetworks()
- if err != nil {
- return nil, fmt.Errorf("erreur lors de la récupération des réseaux: %v", err)
- }
-
- // Convert backend networks to models
- networks := make([]models.WiFiNetwork, 0, len(backendNetworks))
- for _, backendNet := range backendNetworks {
- wifiNet := models.WiFiNetwork{
- SSID: backendNet.SSID,
- Signal: signalToStrength(int(backendNet.SignalDBm)),
- Security: mapSecurityType(backendNet.SecurityType),
- BSSID: backendNet.BSSID,
- Channel: 0, // Not yet exposed by backends
- }
- networks = append(networks, wifiNet)
- }
-
- // Sort by signal strength (descending)
- sort.Slice(networks, func(i, j int) bool {
- return networks[i].Signal > networks[j].Signal
- })
-
- // Broadcast to WebSocket clients if available
- if wifiBroadcaster != nil {
- wifiBroadcaster.BroadcastScanUpdate(networks)
- }
-
- return networks, nil
-}
-
-// Connect connects to a WiFi network
-func Connect(ssid, password string) error {
- // Use backend to connect
- if err := wifiBackend.Connect(ssid, password); err != nil {
- return err
- }
-
- // Poll for connection
- for i := 0; i < 20; i++ {
- time.Sleep(500 * time.Millisecond)
- if IsConnected() {
- return nil
- }
- }
-
- return fmt.Errorf("timeout lors de la connexion")
-}
-
-// Disconnect disconnects from the current WiFi network
-func Disconnect() error {
- return wifiBackend.Disconnect()
-}
-
-// IsConnected checks if WiFi is connected
-func IsConnected() bool {
- state, err := wifiBackend.GetConnectionState()
- if err != nil {
- return false
- }
- return state == backend.StateConnected
-}
-
-// GetConnectedSSID returns the SSID of the currently connected network
-func GetConnectedSSID() string {
- return wifiBackend.GetConnectedSSID()
-}
-
-// GetConnectionState returns the current WiFi connection state
-func GetConnectionState() string {
- state, err := wifiBackend.GetConnectionState()
- if err != nil {
- return string(backend.StateDisconnected)
- }
- return string(state)
-}
-
-// StartEventMonitoring initializes signal monitoring and WebSocket broadcasting
-func StartEventMonitoring() error {
- // Initialize broadcaster
- wifiBroadcaster = NewWifiBroadcaster()
-
- // Set up callbacks
- callbacks := backend.EventCallbacks{
- OnStateChange: handleStateChange,
- OnScanComplete: handleScanComplete,
- }
-
- // Start backend monitoring
- return wifiBackend.StartEventMonitoring(callbacks)
-}
-
-// StopEventMonitoring stops signal monitoring
-func StopEventMonitoring() {
- if wifiBackend != nil {
- wifiBackend.StopEventMonitoring()
- }
-}
-
-// RegisterWebSocketClient registers a new WebSocket client for WiFi events
-func RegisterWebSocketClient(conn *websocket.Conn) {
- if wifiBroadcaster != nil {
- wifiBroadcaster.RegisterClient(conn)
- }
-}
-
-// UnregisterWebSocketClient removes a WebSocket client
-func UnregisterWebSocketClient(conn *websocket.Conn) {
- if wifiBroadcaster != nil {
- wifiBroadcaster.UnregisterClient(conn)
- }
-}
-
-// handleStateChange is called when WiFi connection state changes
-func handleStateChange(newState backend.ConnectionState, connectedSSID string) {
- if wifiBroadcaster != nil {
- wifiBroadcaster.BroadcastStateChange(string(newState), connectedSSID)
- }
-}
-
-// handleScanComplete is called when a WiFi scan completes
-func handleScanComplete() {
- // Get updated network list
- networks, err := GetCachedNetworks()
- if err == nil && wifiBroadcaster != nil {
- wifiBroadcaster.BroadcastScanUpdate(networks)
- }
-}
-
-// mapSecurityType maps backend security types to display format
-func mapSecurityType(securityType string) string {
- switch securityType {
- case "open":
- return "Open"
- case "wep":
- return "WEP"
- case "psk":
- return "WPA2"
- case "8021x":
- return "WPA2"
- default:
- return "WPA2"
- }
-}
-
-// signalToStrength converts signal level (dBm) to strength (1-5)
-func signalToStrength(level int) int {
- if level >= -30 {
- return 5
- } else if level >= -50 {
- return 4
- } else if level >= -60 {
- return 3
- } else if level >= -70 {
- return 2
- } else {
- return 1
- }
-}
diff --git a/internal/wifi/wpasupplicant/backend.go b/internal/wifi/wpasupplicant/backend.go
deleted file mode 100644
index 406b8f5..0000000
--- a/internal/wifi/wpasupplicant/backend.go
+++ /dev/null
@@ -1,265 +0,0 @@
-package wpasupplicant
-
-import (
- "fmt"
- "time"
-
- "github.com/godbus/dbus/v5"
- "github.com/nemunaire/repeater/internal/wifi/backend"
-)
-
-// WPABackend implements the WiFiBackend interface for wpa_supplicant
-type WPABackend struct {
- conn *dbus.Conn
- wpasupplicant dbus.BusObject
- iface *WPAInterface
- signalMonitor *SignalMonitor
- interfaceName string
- currentNetwork dbus.ObjectPath
-}
-
-// NewWPABackend creates a new wpa_supplicant backend instance
-func NewWPABackend() *WPABackend {
- return &WPABackend{}
-}
-
-// Initialize initializes the wpa_supplicant backend with the given interface name
-func (b *WPABackend) Initialize(interfaceName string) error {
- b.interfaceName = interfaceName
- var err error
-
- // Connect to D-Bus
- b.conn, err = dbus.SystemBus()
- if err != nil {
- return fmt.Errorf("failed to connect to D-Bus: %v", err)
- }
-
- // Get wpa_supplicant root object
- b.wpasupplicant = b.conn.Object(Service, dbus.ObjectPath(RootPath))
-
- // Get interface path for the given interface name
- interfacePath, err := b.getInterfacePath(interfaceName)
- if err != nil {
- return fmt.Errorf("failed to get interface for %s: %v", interfaceName, err)
- }
-
- b.iface = NewWPAInterface(b.conn, interfacePath)
-
- return nil
-}
-
-// getInterfacePath gets or creates the wpa_supplicant Interface object path
-func (b *WPABackend) getInterfacePath(interfaceName string) (dbus.ObjectPath, error) {
- var interfacePath dbus.ObjectPath
-
- // Try to get existing interface
- err := b.wpasupplicant.Call(Service+".GetInterface", 0, interfaceName).Store(&interfacePath)
- if err == nil {
- return interfacePath, nil
- }
-
- // Interface doesn't exist, create it
- args := map[string]dbus.Variant{
- "Ifname": dbus.MakeVariant(interfaceName),
- }
-
- err = b.wpasupplicant.Call(Service+".CreateInterface", 0, args).Store(&interfacePath)
- if err != nil {
- return "", fmt.Errorf("failed to create interface: %v", err)
- }
-
- return interfacePath, nil
-}
-
-// Close closes the D-Bus connection
-func (b *WPABackend) Close() error {
- if b.conn != nil {
- b.conn.Close()
- }
- return nil
-}
-
-// ScanNetworks triggers a network scan
-func (b *WPABackend) ScanNetworks() error {
- err := b.iface.Scan("active")
- if err != nil {
- return fmt.Errorf("failed to trigger scan: %v", err)
- }
- return nil
-}
-
-// GetOrderedNetworks returns networks sorted by signal strength in backend-agnostic format
-func (b *WPABackend) GetOrderedNetworks() ([]backend.BackendNetwork, error) {
- // Get BSS list
- bssPaths, err := b.iface.GetBSSs()
- if err != nil {
- return nil, fmt.Errorf("failed to get BSSs: %v", err)
- }
-
- var networks []backend.BackendNetwork
- seenSSIDs := make(map[string]bool)
-
- // Iterate through BSSs and collect network info
- for _, bssPath := range bssPaths {
- bss := NewBSS(b.conn, bssPath)
- props, err := bss.GetProperties()
- if err != nil {
- continue
- }
-
- ssid := string(props.SSID)
- if ssid == "" || seenSSIDs[ssid] {
- continue
- }
- seenSSIDs[ssid] = true
-
- // Get BSSID string
- bssidStr, err := bss.GetBSSIDString()
- if err != nil {
- bssidStr = ""
- }
-
- // Convert to backend-agnostic format
- backendNet := backend.BackendNetwork{
- SSID: ssid,
- SignalDBm: props.Signal,
- SecurityType: props.DetermineSecurityType(),
- BSSID: bssidStr,
- Frequency: props.Frequency,
- }
-
- networks = append(networks, backendNet)
- }
-
- // Sort by signal strength (descending)
- // Note: This is a simple bubble sort for demonstration
- // In production, use sort.Slice
- for i := 0; i < len(networks); i++ {
- for j := i + 1; j < len(networks); j++ {
- if networks[j].SignalDBm > networks[i].SignalDBm {
- networks[i], networks[j] = networks[j], networks[i]
- }
- }
- }
-
- return networks, nil
-}
-
-// IsScanning checks if a scan is currently in progress
-func (b *WPABackend) IsScanning() (bool, error) {
- return b.iface.GetScanning()
-}
-
-// Connect connects to a WiFi network
-func (b *WPABackend) Connect(ssid, password string) error {
- // Create network configuration
- config := make(map[string]interface{})
- config["ssid"] = fmt.Sprintf("\"%s\"", ssid) // wpa_supplicant expects quoted SSID
-
- if password != "" {
- // For WPA/WPA2-PSK networks
- config["psk"] = fmt.Sprintf("\"%s\"", password)
- } else {
- // For open networks
- config["key_mgmt"] = "NONE"
- }
-
- // Add network
- networkPath, err := b.iface.AddNetwork(config)
- if err != nil {
- return fmt.Errorf("failed to add network: %v", err)
- }
-
- // Store current network path for cleanup
- b.currentNetwork = networkPath
-
- // Select (connect to) the network
- err = b.iface.SelectNetwork(networkPath)
- if err != nil {
- // Clean up network on failure
- b.iface.RemoveNetwork(networkPath)
- return fmt.Errorf("failed to select network: %v", err)
- }
-
- return nil
-}
-
-// Disconnect disconnects from the current WiFi network
-func (b *WPABackend) Disconnect() error {
- // Disconnect from current network
- if err := b.iface.Disconnect(); err != nil {
- return fmt.Errorf("failed to disconnect: %v", err)
- }
-
- // Remove the network configuration if we have one
- if b.currentNetwork != "" && b.currentNetwork != "/" {
- b.iface.RemoveNetwork(b.currentNetwork)
- b.currentNetwork = ""
- }
-
- return nil
-}
-
-// GetConnectionState returns the current WiFi connection state
-func (b *WPABackend) GetConnectionState() (backend.ConnectionState, error) {
- state, err := b.iface.GetState()
- if err != nil {
- return backend.StateDisconnected, err
- }
- return mapWPAState(state), nil
-}
-
-// GetConnectedSSID returns the SSID of the currently connected network
-func (b *WPABackend) GetConnectedSSID() string {
- // Get current BSS
- bssPath, err := b.iface.GetCurrentBSS()
- if err != nil || bssPath == "/" {
- return ""
- }
-
- // Get BSS object
- bss := NewBSS(b.conn, bssPath)
- ssid, err := bss.GetSSIDString()
- if err != nil {
- return ""
- }
-
- return ssid
-}
-
-// StartEventMonitoring starts monitoring WiFi events
-func (b *WPABackend) StartEventMonitoring(callbacks backend.EventCallbacks) error {
- // Create signal monitor
- b.signalMonitor = NewSignalMonitor(b.conn, b.iface)
-
- // Start monitoring
- return b.signalMonitor.Start(callbacks)
-}
-
-// StopEventMonitoring stops monitoring WiFi events
-func (b *WPABackend) StopEventMonitoring() {
- if b.signalMonitor != nil {
- b.signalMonitor.Stop()
- }
-}
-
-// Wait for scan to complete (helper method)
-func (b *WPABackend) waitForScanComplete(timeout time.Duration) error {
- start := time.Now()
- for {
- if time.Since(start) > timeout {
- return fmt.Errorf("scan timeout")
- }
-
- scanning, err := b.iface.GetScanning()
- if err != nil {
- return err
- }
-
- if !scanning {
- return nil
- }
-
- time.Sleep(100 * time.Millisecond)
- }
-}
diff --git a/internal/wifi/wpasupplicant/bss.go b/internal/wifi/wpasupplicant/bss.go
deleted file mode 100644
index 799e9f9..0000000
--- a/internal/wifi/wpasupplicant/bss.go
+++ /dev/null
@@ -1,157 +0,0 @@
-package wpasupplicant
-
-import (
- "fmt"
-
- "github.com/godbus/dbus/v5"
-)
-
-// BSS represents a wpa_supplicant BSS (Basic Service Set) object
-type BSS struct {
- path dbus.ObjectPath
- conn *dbus.Conn
- obj dbus.BusObject
-}
-
-// BSSProperties holds the properties of a BSS
-type BSSProperties struct {
- SSID []byte
- BSSID []byte
- Signal int16 // Signal strength in dBm
- Frequency uint32 // Frequency in MHz
- Privacy bool // Whether encryption is enabled
- RSN map[string]dbus.Variant
- WPA map[string]dbus.Variant
-}
-
-// NewBSS creates a new BSS instance
-func NewBSS(conn *dbus.Conn, path dbus.ObjectPath) *BSS {
- return &BSS{
- path: path,
- conn: conn,
- obj: conn.Object(Service, path),
- }
-}
-
-// GetProperties returns all properties of the BSS
-func (b *BSS) GetProperties() (*BSSProperties, error) {
- props := &BSSProperties{}
-
- // Get SSID
- if ssidProp, err := b.obj.GetProperty(BSSInterface + ".SSID"); err == nil {
- if ssid, ok := ssidProp.Value().([]byte); ok {
- props.SSID = ssid
- }
- }
-
- // Get BSSID
- if bssidProp, err := b.obj.GetProperty(BSSInterface + ".BSSID"); err == nil {
- if bssid, ok := bssidProp.Value().([]byte); ok {
- props.BSSID = bssid
- }
- }
-
- // Get Signal
- if signalProp, err := b.obj.GetProperty(BSSInterface + ".Signal"); err == nil {
- if signal, ok := signalProp.Value().(int16); ok {
- props.Signal = signal
- }
- }
-
- // Get Frequency
- if freqProp, err := b.obj.GetProperty(BSSInterface + ".Frequency"); err == nil {
- if freq, ok := freqProp.Value().(uint16); ok {
- props.Frequency = uint32(freq)
- }
- }
-
- // Get Privacy
- if privacyProp, err := b.obj.GetProperty(BSSInterface + ".Privacy"); err == nil {
- if privacy, ok := privacyProp.Value().(bool); ok {
- props.Privacy = privacy
- }
- }
-
- // Get RSN (WPA2) information
- if rsnProp, err := b.obj.GetProperty(BSSInterface + ".RSN"); err == nil {
- if rsn, ok := rsnProp.Value().(map[string]dbus.Variant); ok {
- props.RSN = rsn
- }
- }
-
- // Get WPA information
- if wpaProp, err := b.obj.GetProperty(BSSInterface + ".WPA"); err == nil {
- if wpa, ok := wpaProp.Value().(map[string]dbus.Variant); ok {
- props.WPA = wpa
- }
- }
-
- return props, nil
-}
-
-// GetSSIDString returns the SSID as a string
-func (b *BSS) GetSSIDString() (string, error) {
- prop, err := b.obj.GetProperty(BSSInterface + ".SSID")
- if err != nil {
- return "", fmt.Errorf("failed to get SSID property: %v", err)
- }
-
- ssid, ok := prop.Value().([]byte)
- if !ok {
- return "", fmt.Errorf("SSID property is not a byte array")
- }
-
- return string(ssid), nil
-}
-
-// GetBSSIDString returns the BSSID as a formatted MAC address string
-func (b *BSS) GetBSSIDString() (string, error) {
- prop, err := b.obj.GetProperty(BSSInterface + ".BSSID")
- if err != nil {
- return "", fmt.Errorf("failed to get BSSID property: %v", err)
- }
-
- bssid, ok := prop.Value().([]byte)
- if !ok || len(bssid) != 6 {
- return "", fmt.Errorf("BSSID property is not a valid MAC address")
- }
-
- return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x",
- bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]), nil
-}
-
-// GetSignal returns the signal strength in dBm
-func (b *BSS) GetSignal() (int16, error) {
- prop, err := b.obj.GetProperty(BSSInterface + ".Signal")
- if err != nil {
- return 0, fmt.Errorf("failed to get Signal property: %v", err)
- }
-
- signal, ok := prop.Value().(int16)
- if !ok {
- return 0, fmt.Errorf("Signal property is not an int16")
- }
-
- return signal, nil
-}
-
-// DetermineSecurityType determines the security type based on BSS properties
-func (p *BSSProperties) DetermineSecurityType() string {
- // Check for WPA2 (RSN)
- if len(p.RSN) > 0 {
- return "psk"
- }
-
- // Check for WPA
- if len(p.WPA) > 0 {
- return "psk"
- }
-
- // Check for WEP (privacy but no WPA/RSN)
- if p.Privacy {
- return "wep"
- }
-
- // Open network
- return "open"
-}
diff --git a/internal/wifi/wpasupplicant/interface.go b/internal/wifi/wpasupplicant/interface.go
deleted file mode 100644
index ecc9fbb..0000000
--- a/internal/wifi/wpasupplicant/interface.go
+++ /dev/null
@@ -1,146 +0,0 @@
-package wpasupplicant
-
-import (
- "fmt"
-
- "github.com/godbus/dbus/v5"
-)
-
-// WPAInterface represents a wpa_supplicant Interface object
-type WPAInterface struct {
- path dbus.ObjectPath
- conn *dbus.Conn
- obj dbus.BusObject
-}
-
-// NewWPAInterface creates a new WPAInterface instance
-func NewWPAInterface(conn *dbus.Conn, path dbus.ObjectPath) *WPAInterface {
- return &WPAInterface{
- path: path,
- conn: conn,
- obj: conn.Object(Service, path),
- }
-}
-
-// Scan triggers a network scan
-func (i *WPAInterface) Scan(scanType string) error {
- args := map[string]interface{}{
- "Type": scanType, // "active" or "passive"
- }
-
- err := i.obj.Call(InterfaceInterface+".Scan", 0, args).Err
- if err != nil {
- return fmt.Errorf("scan failed: %v", err)
- }
- return nil
-}
-
-// GetBSSs returns a list of BSS (Basic Service Set) object paths
-func (i *WPAInterface) GetBSSs() ([]dbus.ObjectPath, error) {
- prop, err := i.obj.GetProperty(InterfaceInterface + ".BSSs")
- if err != nil {
- return nil, fmt.Errorf("failed to get BSSs property: %v", err)
- }
-
- bsss, ok := prop.Value().([]dbus.ObjectPath)
- if !ok {
- return nil, fmt.Errorf("BSSs property is not an array of ObjectPath")
- }
-
- return bsss, nil
-}
-
-// GetState returns the current connection state
-func (i *WPAInterface) GetState() (WPAState, error) {
- prop, err := i.obj.GetProperty(InterfaceInterface + ".State")
- if err != nil {
- return "", fmt.Errorf("failed to get State property: %v", err)
- }
-
- state, ok := prop.Value().(string)
- if !ok {
- return "", fmt.Errorf("State property is not a string")
- }
-
- return WPAState(state), nil
-}
-
-// GetCurrentBSS returns the currently connected BSS object path
-func (i *WPAInterface) GetCurrentBSS() (dbus.ObjectPath, error) {
- prop, err := i.obj.GetProperty(InterfaceInterface + ".CurrentBSS")
- if err != nil {
- return "", fmt.Errorf("failed to get CurrentBSS property: %v", err)
- }
-
- bss, ok := prop.Value().(dbus.ObjectPath)
- if !ok {
- return "", fmt.Errorf("CurrentBSS property is not an ObjectPath")
- }
-
- return bss, nil
-}
-
-// AddNetwork creates a new network configuration
-func (i *WPAInterface) AddNetwork(config map[string]interface{}) (dbus.ObjectPath, error) {
- var networkPath dbus.ObjectPath
-
- // Convert config to proper DBus variant format
- dbusConfig := make(map[string]dbus.Variant)
- for key, value := range config {
- dbusConfig[key] = dbus.MakeVariant(value)
- }
-
- err := i.obj.Call(InterfaceInterface+".AddNetwork", 0, dbusConfig).Store(&networkPath)
- if err != nil {
- return "", fmt.Errorf("failed to add network: %v", err)
- }
-
- return networkPath, nil
-}
-
-// SelectNetwork connects to a network
-func (i *WPAInterface) SelectNetwork(networkPath dbus.ObjectPath) error {
- err := i.obj.Call(InterfaceInterface+".SelectNetwork", 0, networkPath).Err
- if err != nil {
- return fmt.Errorf("failed to select network: %v", err)
- }
- return nil
-}
-
-// RemoveNetwork removes a network configuration
-func (i *WPAInterface) RemoveNetwork(networkPath dbus.ObjectPath) error {
- err := i.obj.Call(InterfaceInterface+".RemoveNetwork", 0, networkPath).Err
- if err != nil {
- return fmt.Errorf("failed to remove network: %v", err)
- }
- return nil
-}
-
-// Disconnect disconnects from the current network
-func (i *WPAInterface) Disconnect() error {
- err := i.obj.Call(InterfaceInterface+".Disconnect", 0).Err
- if err != nil {
- return fmt.Errorf("disconnect failed: %v", err)
- }
- return nil
-}
-
-// GetPath returns the D-Bus object path for this interface
-func (i *WPAInterface) GetPath() dbus.ObjectPath {
- return i.path
-}
-
-// GetScanning returns whether a scan is currently in progress
-func (i *WPAInterface) GetScanning() (bool, error) {
- prop, err := i.obj.GetProperty(InterfaceInterface + ".Scanning")
- if err != nil {
- return false, fmt.Errorf("failed to get Scanning property: %v", err)
- }
-
- scanning, ok := prop.Value().(bool)
- if !ok {
- return false, fmt.Errorf("Scanning property is not a boolean")
- }
-
- return scanning, nil
-}
diff --git a/internal/wifi/wpasupplicant/network.go b/internal/wifi/wpasupplicant/network.go
deleted file mode 100644
index 3152a6b..0000000
--- a/internal/wifi/wpasupplicant/network.go
+++ /dev/null
@@ -1,41 +0,0 @@
-package wpasupplicant
-
-import (
- "github.com/godbus/dbus/v5"
-)
-
-// Network represents a wpa_supplicant Network configuration object
-type Network struct {
- path dbus.ObjectPath
- conn *dbus.Conn
- obj dbus.BusObject
-}
-
-// NewNetwork creates a new Network instance
-func NewNetwork(conn *dbus.Conn, path dbus.ObjectPath) *Network {
- return &Network{
- path: path,
- conn: conn,
- obj: conn.Object(Service, path),
- }
-}
-
-// GetPath returns the D-Bus object path for this network
-func (n *Network) GetPath() dbus.ObjectPath {
- return n.path
-}
-
-// GetProperties returns properties of the network configuration
-func (n *Network) GetProperties() (map[string]dbus.Variant, error) {
- prop, err := n.obj.GetProperty(NetworkInterface + ".Properties")
- if err != nil {
- return nil, err
- }
-
- props, ok := prop.Value().(map[string]dbus.Variant)
- if !ok {
- return nil, nil
- }
-
- return props, nil
-}
diff --git a/internal/wifi/wpasupplicant/signals.go b/internal/wifi/wpasupplicant/signals.go
deleted file mode 100644
index a17a292..0000000
--- a/internal/wifi/wpasupplicant/signals.go
+++ /dev/null
@@ -1,236 +0,0 @@
-package wpasupplicant
-
-import (
- "log"
- "sync"
-
- "github.com/godbus/dbus/v5"
- "github.com/nemunaire/repeater/internal/wifi/backend"
-)
-
-// SignalMonitor monitors D-Bus signals from wpa_supplicant
-type SignalMonitor struct {
- conn *dbus.Conn
- iface *WPAInterface
- callbacks backend.EventCallbacks
-
- // Signal channel
- signalChan chan *dbus.Signal
-
- // Control
- stopChan chan struct{}
- mu sync.RWMutex
- running bool
-
- // State tracking
- lastState WPAState
-}
-
-// NewSignalMonitor creates a new signal monitor
-func NewSignalMonitor(conn *dbus.Conn, iface *WPAInterface) *SignalMonitor {
- return &SignalMonitor{
- conn: conn,
- iface: iface,
- signalChan: make(chan *dbus.Signal, 100),
- stopChan: make(chan struct{}),
- }
-}
-
-// Start begins monitoring D-Bus signals
-func (sm *SignalMonitor) Start(callbacks backend.EventCallbacks) error {
- sm.mu.Lock()
- if sm.running {
- sm.mu.Unlock()
- return nil
- }
- sm.running = true
- sm.callbacks = callbacks
- sm.mu.Unlock()
-
- interfacePath := sm.iface.GetPath()
-
- // Add signal match for PropertiesChanged on Interface
- matchOptions := []dbus.MatchOption{
- dbus.WithMatchObjectPath(interfacePath),
- dbus.WithMatchInterface("org.freedesktop.DBus.Properties"),
- dbus.WithMatchMember("PropertiesChanged"),
- }
-
- if err := sm.conn.AddMatchSignal(matchOptions...); err != nil {
- sm.mu.Lock()
- sm.running = false
- sm.mu.Unlock()
- return err
- }
-
- // Add signal match for ScanDone
- scanDoneOptions := []dbus.MatchOption{
- dbus.WithMatchObjectPath(interfacePath),
- dbus.WithMatchInterface(InterfaceInterface),
- dbus.WithMatchMember("ScanDone"),
- }
-
- if err := sm.conn.AddMatchSignal(scanDoneOptions...); err != nil {
- sm.mu.Lock()
- sm.running = false
- sm.mu.Unlock()
- return err
- }
-
- // Register signal channel
- sm.conn.Signal(sm.signalChan)
-
- // Get initial state
- state, err := sm.iface.GetState()
- if err == nil {
- sm.lastState = state
- }
-
- // Start monitoring goroutine
- go sm.monitor()
-
- log.Printf("D-Bus signal monitoring started for wpa_supplicant interface %s", interfacePath)
- return nil
-}
-
-// Stop stops monitoring D-Bus signals
-func (sm *SignalMonitor) Stop() {
- sm.mu.Lock()
- if !sm.running {
- sm.mu.Unlock()
- return
- }
- sm.running = false
- sm.mu.Unlock()
-
- // Signal stop
- close(sm.stopChan)
-
- // Remove signal channel
- sm.conn.RemoveSignal(sm.signalChan)
-
- log.Printf("D-Bus signal monitoring stopped for wpa_supplicant")
-}
-
-// monitor is the main signal processing loop
-func (sm *SignalMonitor) monitor() {
- for {
- select {
- case sig := <-sm.signalChan:
- sm.handleSignal(sig)
- case <-sm.stopChan:
- return
- }
- }
-}
-
-// handleSignal processes a D-Bus signal
-func (sm *SignalMonitor) handleSignal(sig *dbus.Signal) {
- // Handle ScanDone signal
- if sig.Name == InterfaceInterface+".ScanDone" {
- sm.handleScanDone(sig)
- return
- }
-
- // Handle PropertiesChanged signals
- if sig.Name != "org.freedesktop.DBus.Properties.PropertiesChanged" {
- return
- }
-
- // Verify signal is from Interface
- if len(sig.Body) < 2 {
- return
- }
-
- interfaceName, ok := sig.Body[0].(string)
- if !ok || interfaceName != InterfaceInterface {
- return
- }
-
- // Parse changed properties
- changedProps, ok := sig.Body[1].(map[string]dbus.Variant)
- if !ok {
- return
- }
-
- // Check for State property change
- if stateVariant, ok := changedProps["State"]; ok {
- if state, ok := stateVariant.Value().(string); ok {
- sm.handleStateChange(WPAState(state))
- }
- }
-
- // Check for CurrentBSS property change (connection status)
- if _, ok := changedProps["CurrentBSS"]; ok {
- // BSS changed, trigger state update
- sm.handleConnectionChange()
- }
-}
-
-// handleStateChange processes a state change
-func (sm *SignalMonitor) handleStateChange(state WPAState) {
- sm.lastState = state
-
- sm.mu.RLock()
- callback := sm.callbacks.OnStateChange
- sm.mu.RUnlock()
-
- if callback == nil {
- return
- }
-
- // Map wpa_supplicant state to backend state
- backendState := mapWPAState(state)
-
- // Get connected SSID if connected
- ssid := ""
- if backendState == backend.StateConnected {
- if bssPath, err := sm.iface.GetCurrentBSS(); err == nil && bssPath != "/" {
- bss := NewBSS(sm.conn, bssPath)
- if ssidStr, err := bss.GetSSIDString(); err == nil {
- ssid = ssidStr
- }
- }
- }
-
- callback(backendState, ssid)
-}
-
-// handleConnectionChange processes connection changes
-func (sm *SignalMonitor) handleConnectionChange() {
- // Get current state and trigger state change callback
- state, err := sm.iface.GetState()
- if err != nil {
- return
- }
-
- sm.handleStateChange(state)
-}
-
-// handleScanDone processes scan completion
-func (sm *SignalMonitor) handleScanDone(sig *dbus.Signal) {
- sm.mu.RLock()
- callback := sm.callbacks.OnScanComplete
- sm.mu.RUnlock()
-
- if callback != nil {
- callback()
- }
-}
-
-// mapWPAState maps wpa_supplicant states to backend-agnostic states
-func mapWPAState(state WPAState) backend.ConnectionState {
- switch state {
- case StateCompleted:
- return backend.StateConnected
- case StateAuthenticating, StateAssociating, StateAssociated, State4WayHandshake, StateGroupHandshake:
- return backend.StateConnecting
- case StateDisconnected, StateInactive, StateInterfaceDisabled:
- return backend.StateDisconnected
- case StateScanning:
- // Keep as disconnected if just scanning
- return backend.StateDisconnected
- default:
- return backend.StateDisconnected
- }
-}
diff --git a/internal/wifi/wpasupplicant/types.go b/internal/wifi/wpasupplicant/types.go
deleted file mode 100644
index 85fec0f..0000000
--- a/internal/wifi/wpasupplicant/types.go
+++ /dev/null
@@ -1,27 +0,0 @@
-package wpasupplicant
-
-const (
- // D-Bus service and interfaces
- Service = "fi.w1.wpa_supplicant1"
- RootPath = "/fi/w1/wpa_supplicant1"
- InterfaceInterface = "fi.w1.wpa_supplicant1.Interface"
- BSSInterface = "fi.w1.wpa_supplicant1.BSS"
- NetworkInterface = "fi.w1.wpa_supplicant1.Network"
-)
-
-// WPAState represents the wpa_supplicant connection state
-type WPAState string
-
-const (
- // wpa_supplicant state strings
- StateDisconnected WPAState = "disconnected"
- StateInactive WPAState = "inactive"
- StateScanning WPAState = "scanning"
- StateAuthenticating WPAState = "authenticating"
- StateAssociating WPAState = "associating"
- StateAssociated WPAState = "associated"
- State4WayHandshake WPAState = "4way_handshake"
- StateGroupHandshake WPAState = "group_handshake"
- StateCompleted WPAState = "completed"
- StateInterfaceDisabled WPAState = "interface_disabled"
-)
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..c8b9fe7
--- /dev/null
+++ b/main.go
@@ -0,0 +1,826 @@
+package main
+
+import (
+ "bufio"
+ "embed"
+ "encoding/json"
+ "fmt"
+ "io/fs"
+ "log"
+ "net/http"
+ "os"
+ "os/exec"
+ "regexp"
+ "sort"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/godbus/dbus/v5"
+ "github.com/gorilla/mux"
+ "github.com/gorilla/websocket"
+)
+
+//go:embed all:static
+var _assets embed.FS
+
+// Structures de données
+type WiFiNetwork struct {
+ SSID string `json:"ssid"`
+ Signal int `json:"signal"`
+ Security string `json:"security"`
+ Channel int `json:"channel"`
+ BSSID string `json:"bssid"`
+}
+
+type ConnectedDevice struct {
+ Name string `json:"name"`
+ Type string `json:"type"`
+ MAC string `json:"mac"`
+ IP string `json:"ip"`
+}
+
+type HotspotConfig struct {
+ SSID string `json:"ssid"`
+ Password string `json:"password"`
+ Channel int `json:"channel"`
+}
+
+type SystemStatus struct {
+ Connected bool `json:"connected"`
+ ConnectedSSID string `json:"connectedSSID"`
+ HotspotEnabled bool `json:"hotspotEnabled"`
+ ConnectedCount int `json:"connectedCount"`
+ DataUsage float64 `json:"dataUsage"`
+ Uptime int64 `json:"uptime"`
+ ConnectedDevices []ConnectedDevice `json:"connectedDevices"`
+}
+
+type WiFiConnectRequest struct {
+ SSID string `json:"ssid"`
+ Password string `json:"password"`
+}
+
+type LogEntry struct {
+ Timestamp time.Time `json:"timestamp"`
+ Source string `json:"source"`
+ Message string `json:"message"`
+}
+
+// Variables globales
+var (
+ currentStatus SystemStatus
+ statusMutex sync.RWMutex
+ logEntries []LogEntry
+ logMutex sync.RWMutex
+ websocketClients = make(map[*websocket.Conn]bool)
+ clientsMutex sync.RWMutex
+ upgrader = websocket.Upgrader{
+ CheckOrigin: func(r *http.Request) bool {
+ return true
+ },
+ }
+ startTime = time.Now()
+ dbusConn *dbus.Conn
+ wpaSupplicant dbus.BusObject
+)
+
+const (
+ WLAN_INTERFACE = "wlan0"
+ AP_INTERFACE = "wlan1"
+ HOSTAPD_CONF = "/etc/hostapd/hostapd.conf"
+ WPA_CONF = "/etc/wpa_supplicant/wpa_supplicant.conf"
+
+ // D-Bus constantes pour wpa_supplicant
+ WPA_SUPPLICANT_SERVICE = "fi.w1.wpa_supplicant1"
+ WPA_SUPPLICANT_PATH = "/fi/w1/wpa_supplicant1"
+ WPA_SUPPLICANT_IFACE = "fi.w1.wpa_supplicant1"
+ WPA_INTERFACE_IFACE = "fi.w1.wpa_supplicant1.Interface"
+ WPA_BSS_IFACE = "fi.w1.wpa_supplicant1.BSS"
+ WPA_NETWORK_IFACE = "fi.w1.wpa_supplicant1.Network"
+)
+
+func main() {
+ // Initialiser D-Bus
+ var err error
+ dbusConn, err = dbus.SystemBus()
+ if err != nil {
+ log.Fatalf("Erreur de connexion D-Bus: %v", err)
+ }
+ defer dbusConn.Close()
+
+ wpaSupplicant = dbusConn.Object(WPA_SUPPLICANT_SERVICE, dbus.ObjectPath(WPA_SUPPLICANT_PATH))
+
+ // Initialiser le statut système
+ initializeStatus()
+
+ // Démarrer les tâches périodiques
+ go periodicStatusUpdate()
+ go periodicDeviceUpdate()
+
+ // Configuration du routeur
+ r := mux.NewRouter()
+
+ // Routes API
+ api := r.PathPrefix("/api").Subrouter()
+ api.HandleFunc("/wifi/scan", scanWiFiHandler).Methods("GET")
+ api.HandleFunc("/wifi/connect", connectWiFiHandler).Methods("POST")
+ api.HandleFunc("/wifi/disconnect", disconnectWiFiHandler).Methods("POST")
+ api.HandleFunc("/hotspot/config", configureHotspotHandler).Methods("POST")
+ api.HandleFunc("/hotspot/toggle", toggleHotspotHandler).Methods("POST")
+ api.HandleFunc("/devices", getDevicesHandler).Methods("GET")
+ api.HandleFunc("/status", getStatusHandler).Methods("GET")
+ api.HandleFunc("/logs", getLogsHandler).Methods("GET")
+ api.HandleFunc("/logs", clearLogsHandler).Methods("DELETE")
+
+ // WebSocket pour les logs en temps réel
+ r.HandleFunc("/ws/logs", websocketHandler)
+
+ // Servir les fichiers statiques
+ sub, err := fs.Sub(_assets, "static")
+ if err != nil {
+ log.Fatal("Unable to cd to static/ directory:", err)
+ }
+ Assets := http.FS(sub)
+ r.PathPrefix("/").Handler(http.FileServer(Assets))
+
+ addLog("Système", "Serveur API démarré sur le port 8080")
+ log.Fatal(http.ListenAndServe(":8080", r))
+}
+
+func initializeStatus() {
+ statusMutex.Lock()
+ defer statusMutex.Unlock()
+
+ currentStatus = SystemStatus{
+ Connected: false,
+ ConnectedSSID: "",
+ HotspotEnabled: true,
+ ConnectedCount: 0,
+ DataUsage: 0.0,
+ Uptime: 0,
+ }
+}
+
+// Handlers API
+
+func scanWiFiHandler(w http.ResponseWriter, r *http.Request) {
+ networks, err := scanWiFiNetworks()
+ if err != nil {
+ addLog("WiFi", fmt.Sprintf("Erreur lors du scan: %v", err))
+ http.Error(w, "Erreur lors du scan WiFi", http.StatusInternalServerError)
+ return
+ }
+
+ addLog("WiFi", fmt.Sprintf("Scan terminé - %d réseaux trouvés", len(networks)))
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(networks)
+}
+
+func connectWiFiHandler(w http.ResponseWriter, r *http.Request) {
+ var req WiFiConnectRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ http.Error(w, "Données invalides", http.StatusBadRequest)
+ return
+ }
+
+ addLog("WiFi", fmt.Sprintf("Tentative de connexion à %s", req.SSID))
+
+ err := connectToWiFiDBus(req.SSID, req.Password)
+ if err != nil {
+ addLog("WiFi", fmt.Sprintf("Échec de connexion: %v", err))
+ http.Error(w, fmt.Sprintf("Échec de connexion: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ statusMutex.Lock()
+ currentStatus.Connected = true
+ currentStatus.ConnectedSSID = req.SSID
+ statusMutex.Unlock()
+
+ addLog("WiFi", fmt.Sprintf("Connexion réussie à %s", req.SSID))
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]string{"status": "success"})
+}
+
+func disconnectWiFiHandler(w http.ResponseWriter, r *http.Request) {
+ addLog("WiFi", "Tentative de déconnexion")
+
+ err := disconnectWiFiDBus()
+ if err != nil {
+ addLog("WiFi", fmt.Sprintf("Erreur de déconnexion: %v", err))
+ http.Error(w, fmt.Sprintf("Erreur de déconnexion: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ statusMutex.Lock()
+ currentStatus.Connected = false
+ currentStatus.ConnectedSSID = ""
+ statusMutex.Unlock()
+
+ addLog("WiFi", "Déconnexion réussie")
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]string{"status": "success"})
+}
+
+func configureHotspotHandler(w http.ResponseWriter, r *http.Request) {
+ var config HotspotConfig
+ if err := json.NewDecoder(r.Body).Decode(&config); err != nil {
+ http.Error(w, "Données invalides", http.StatusBadRequest)
+ return
+ }
+
+ err := configureHotspot(config)
+ if err != nil {
+ addLog("Hotspot", fmt.Sprintf("Erreur de configuration: %v", err))
+ http.Error(w, fmt.Sprintf("Erreur de configuration: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ addLog("Hotspot", fmt.Sprintf("Configuration mise à jour: %s (Canal %d)", config.SSID, config.Channel))
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]string{"status": "success"})
+}
+
+func toggleHotspotHandler(w http.ResponseWriter, r *http.Request) {
+ statusMutex.Lock()
+ currentStatus.HotspotEnabled = !currentStatus.HotspotEnabled
+ enabled := currentStatus.HotspotEnabled
+ statusMutex.Unlock()
+
+ var err error
+ if enabled {
+ err = startHotspot()
+ addLog("Hotspot", "Hotspot activé")
+ } else {
+ err = stopHotspot()
+ addLog("Hotspot", "Hotspot désactivé")
+ }
+
+ if err != nil {
+ addLog("Hotspot", fmt.Sprintf("Erreur: %v", err))
+ http.Error(w, fmt.Sprintf("Erreur: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]bool{"enabled": enabled})
+}
+
+func getDevicesHandler(w http.ResponseWriter, r *http.Request) {
+ devices, err := getConnectedDevices()
+ if err != nil {
+ addLog("Système", fmt.Sprintf("Erreur lors de la récupération des appareils: %v", err))
+ http.Error(w, "Erreur lors de la récupération des appareils", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(devices)
+}
+
+func getStatusHandler(w http.ResponseWriter, r *http.Request) {
+ statusMutex.RLock()
+ status := currentStatus
+ status.Uptime = int64(time.Since(startTime).Seconds())
+ statusMutex.RUnlock()
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(status)
+}
+
+func getLogsHandler(w http.ResponseWriter, r *http.Request) {
+ logMutex.RLock()
+ logs := make([]LogEntry, len(logEntries))
+ copy(logs, logEntries)
+ logMutex.RUnlock()
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(logs)
+}
+
+func clearLogsHandler(w http.ResponseWriter, r *http.Request) {
+ logMutex.Lock()
+ logEntries = []LogEntry{}
+ logMutex.Unlock()
+
+ addLog("Système", "Logs effacés")
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]string{"status": "success"})
+}
+
+// Fonctions WiFi avec D-Bus
+
+func scanWiFiNetworks() ([]WiFiNetwork, error) {
+ interfacePath, err := getWiFiInterfacePath()
+ if err != nil {
+ return nil, fmt.Errorf("impossible d'obtenir l'interface WiFi: %v", err)
+ }
+
+ // Déclencher un scan
+ wifiInterface := dbusConn.Object(WPA_SUPPLICANT_SERVICE, interfacePath)
+ call := wifiInterface.Call(WPA_INTERFACE_IFACE+".Scan", 0, map[string]dbus.Variant{"Type": dbus.MakeVariant("active")})
+ if call.Err != nil {
+ return nil, fmt.Errorf("erreur lors du scan: %v", call.Err)
+ }
+
+ // Attendre un peu pour que le scan se termine
+ time.Sleep(2 * time.Second)
+
+ // Récupérer la liste des BSS
+ bssePaths, err := wifiInterface.GetProperty(WPA_INTERFACE_IFACE + ".BSSs")
+ if err != nil {
+ return nil, fmt.Errorf("erreur lors de la récupération des BSS: %v", err)
+ }
+
+ var networks []WiFiNetwork
+ seenSSIDs := make(map[string]bool)
+
+ for _, bssPath := range bssePaths.Value().([]dbus.ObjectPath) {
+ bss := dbusConn.Object(WPA_SUPPLICANT_SERVICE, bssPath)
+
+ // Récupérer les propriétés du BSS
+ var props map[string]dbus.Variant
+ err = bss.Call("org.freedesktop.DBus.Properties.GetAll", 0, WPA_BSS_IFACE).Store(&props)
+ if err != nil {
+ continue
+ }
+
+ network := WiFiNetwork{}
+
+ // Extraire SSID
+ if ssidBytes, ok := props["SSID"].Value().([]byte); ok {
+ network.SSID = string(ssidBytes)
+ }
+
+ // Éviter les doublons
+ if network.SSID == "" || seenSSIDs[network.SSID] {
+ continue
+ }
+ seenSSIDs[network.SSID] = true
+
+ // Extraire BSSID
+ if bssidBytes, ok := props["BSSID"].Value().([]byte); ok {
+ network.BSSID = fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x",
+ bssidBytes[0], bssidBytes[1], bssidBytes[2], bssidBytes[3], bssidBytes[4], bssidBytes[5])
+ }
+
+ // Extraire la force du signal
+ if signal, ok := props["Signal"].Value().(int16); ok {
+ network.Signal = signalToStrength(int(signal))
+ }
+
+ // Extraire la fréquence et calculer le canal
+ if frequency, ok := props["Frequency"].Value().(uint16); ok {
+ network.Channel = frequencyToChannel(int(frequency))
+ }
+
+ // Déterminer la sécurité
+ if privacyVal, ok := props["Privacy"].Value().(bool); ok && privacyVal {
+ if wpaProps, ok := props["WPA"].Value().(map[string]dbus.Variant); ok && len(wpaProps) > 0 {
+ network.Security = "WPA"
+ } else if rsnProps, ok := props["RSN"].Value().(map[string]dbus.Variant); ok && len(rsnProps) > 0 {
+ network.Security = "WPA2"
+ } else {
+ network.Security = "WEP"
+ }
+ } else {
+ network.Security = "Open"
+ }
+
+ networks = append(networks, network)
+ }
+
+ // Trier par force du signal
+ sort.Slice(networks, func(i, j int) bool {
+ return networks[i].Signal > networks[j].Signal
+ })
+
+ return networks, nil
+}
+
+func connectToWiFiDBus(ssid, password string) error {
+ interfacePath, err := getWiFiInterfacePath()
+ if err != nil {
+ return fmt.Errorf("impossible d'obtenir l'interface WiFi: %v", err)
+ }
+
+ wifiInterface := dbusConn.Object(WPA_SUPPLICANT_SERVICE, interfacePath)
+
+ // Créer un nouveau réseau
+ networkConfig := map[string]dbus.Variant{
+ "ssid": dbus.MakeVariant(ssid),
+ }
+
+ if password != "" {
+ networkConfig["psk"] = dbus.MakeVariant(password)
+ }
+
+ var networkPath dbus.ObjectPath
+ err = wifiInterface.Call(WPA_INTERFACE_IFACE+".AddNetwork", 0, networkConfig).Store(&networkPath)
+ if err != nil {
+ return fmt.Errorf("erreur lors de l'ajout du réseau: %v", err)
+ }
+
+ // Sélectionner le réseau
+ err = wifiInterface.Call(WPA_INTERFACE_IFACE+".SelectNetwork", 0, networkPath).Err
+ if err != nil {
+ return fmt.Errorf("erreur lors de la sélection du réseau: %v", err)
+ }
+
+ // Attendre la connexion
+ for i := 0; i < 20; i++ {
+ time.Sleep(500 * time.Millisecond)
+ if isConnectedDBus() {
+ return nil
+ }
+ }
+
+ return fmt.Errorf("timeout lors de la connexion")
+}
+
+func disconnectWiFiDBus() error {
+ interfacePath, err := getWiFiInterfacePath()
+ if err != nil {
+ return fmt.Errorf("impossible d'obtenir l'interface WiFi: %v", err)
+ }
+
+ wifiInterface := dbusConn.Object(WPA_SUPPLICANT_SERVICE, interfacePath)
+
+ // Déconnecter
+ err = wifiInterface.Call(WPA_INTERFACE_IFACE+".Disconnect", 0).Err
+ if err != nil {
+ return fmt.Errorf("erreur lors de la déconnexion: %v", err)
+ }
+
+ // Supprimer tous les réseaux
+ var networks []dbus.ObjectPath
+ err = wifiInterface.Call(WPA_INTERFACE_IFACE+".Get", 0, WPA_INTERFACE_IFACE, "Networks").Store(&networks)
+ if err == nil {
+ for _, networkPath := range networks {
+ wifiInterface.Call(WPA_INTERFACE_IFACE+".RemoveNetwork", 0, networkPath)
+ }
+ }
+
+ return nil
+}
+
+func getWiFiInterfacePath() (dbus.ObjectPath, error) {
+ var interfacePath dbus.ObjectPath
+ err := wpaSupplicant.Call(WPA_SUPPLICANT_IFACE+".GetInterface", 0, WLAN_INTERFACE).Store(&interfacePath)
+ if err != nil {
+ return "", fmt.Errorf("erreur lors de la récupération des interfaces: %v", err)
+ }
+
+ return interfacePath, nil
+}
+
+func isConnectedDBus() bool {
+ interfacePath, err := getWiFiInterfacePath()
+ if err != nil {
+ return false
+ }
+
+ wifiInterface := dbusConn.Object(WPA_SUPPLICANT_SERVICE, interfacePath)
+ var state string
+ err = wifiInterface.Call(WPA_INTERFACE_IFACE+".Get", 0, WPA_INTERFACE_IFACE, "State").Store(&state)
+ if err != nil {
+ return false
+ }
+
+ return state == "completed"
+}
+
+func frequencyToChannel(frequency int) int {
+ if frequency >= 2412 && frequency <= 2484 {
+ if frequency == 2484 {
+ return 14
+ }
+ return (frequency-2412)/5 + 1
+ } else if frequency >= 5170 && frequency <= 5825 {
+ return (frequency - 5000) / 5
+ }
+ return 0
+}
+
+func signalToStrength(level int) int {
+ if level >= -30 {
+ return 5
+ } else if level >= -50 {
+ return 4
+ } else if level >= -60 {
+ return 3
+ } else if level >= -70 {
+ return 2
+ } else {
+ return 1
+ }
+}
+
+func connectToWiFi(ssid, password string) error {
+ // Créer la configuration wpa_supplicant
+ config := fmt.Sprintf(`ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
+update_config=1
+country=FR
+
+network={
+ ssid="%s"
+ psk="%s"
+}
+`, ssid, password)
+
+ err := os.WriteFile(WPA_CONF, []byte(config), 0600)
+ if err != nil {
+ return err
+ }
+
+ // Redémarrer wpa_supplicant
+ cmd := exec.Command("systemctl", "restart", "wpa_supplicant")
+ if err := cmd.Run(); err != nil {
+ return err
+ }
+
+ // Attendre la connexion
+ for i := 0; i < 10; i++ {
+ time.Sleep(1 * time.Second)
+ if isConnected() {
+ return nil
+ }
+ }
+
+ return fmt.Errorf("timeout lors de la connexion")
+}
+
+func isConnected() bool {
+ cmd := exec.Command("iwconfig", WLAN_INTERFACE)
+ output, err := cmd.Output()
+ if err != nil {
+ return false
+ }
+
+ return strings.Contains(string(output), "Access Point:")
+}
+
+// Fonctions Hotspot
+
+func configureHotspot(config HotspotConfig) error {
+ hostapdConfig := fmt.Sprintf(`interface=%s
+driver=nl80211
+ssid=%s
+hw_mode=g
+channel=%d
+wmm_enabled=0
+macaddr_acl=0
+auth_algs=1
+ignore_broadcast_ssid=0
+wpa=2
+wpa_passphrase=%s
+wpa_key_mgmt=WPA-PSK
+wpa_pairwise=TKIP
+rsn_pairwise=CCMP
+`, AP_INTERFACE, config.SSID, config.Channel, config.Password)
+
+ return os.WriteFile(HOSTAPD_CONF, []byte(hostapdConfig), 0644)
+}
+
+func startHotspot() error {
+ cmd := exec.Command("systemctl", "start", "hostapd")
+ return cmd.Run()
+}
+
+func stopHotspot() error {
+ cmd := exec.Command("systemctl", "stop", "hostapd")
+ return cmd.Run()
+}
+
+// Fonctions pour les appareils connectés
+
+func getConnectedDevices() ([]ConnectedDevice, error) {
+ var devices []ConnectedDevice
+
+ // Lire les baux DHCP
+ leases, err := parseDHCPLeases()
+ if err != nil {
+ return devices, err
+ }
+
+ // Obtenir les informations ARP
+ arpInfo, err := getARPInfo()
+ if err != nil {
+ return devices, err
+ }
+
+ for _, lease := range leases {
+ device := ConnectedDevice{
+ Name: lease.Hostname,
+ MAC: lease.MAC,
+ IP: lease.IP,
+ Type: guessDeviceType(lease.Hostname, lease.MAC),
+ }
+
+ // Vérifier si l'appareil est toujours connecté via ARP
+ if _, exists := arpInfo[lease.IP]; exists {
+ devices = append(devices, device)
+ }
+ }
+
+ return devices, nil
+}
+
+type DHCPLease struct {
+ IP string
+ MAC string
+ Hostname string
+}
+
+func parseDHCPLeases() ([]DHCPLease, error) {
+ var leases []DHCPLease
+
+ file, err := os.Open("/var/lib/dhcp/dhcpd.leases")
+ if err != nil {
+ return leases, err
+ }
+ defer file.Close()
+
+ scanner := bufio.NewScanner(file)
+ var currentLease DHCPLease
+
+ for scanner.Scan() {
+ line := strings.TrimSpace(scanner.Text())
+
+ if strings.HasPrefix(line, "lease ") {
+ ip := strings.Fields(line)[1]
+ currentLease = DHCPLease{IP: ip}
+ } else if strings.Contains(line, "hardware ethernet") {
+ mac := strings.Fields(line)[2]
+ mac = strings.TrimSuffix(mac, ";")
+ currentLease.MAC = mac
+ } else if strings.Contains(line, "client-hostname") {
+ hostname := strings.Fields(line)[1]
+ hostname = strings.Trim(hostname, `";`)
+ currentLease.Hostname = hostname
+ } else if line == "}" {
+ if currentLease.IP != "" && currentLease.MAC != "" {
+ leases = append(leases, currentLease)
+ }
+ currentLease = DHCPLease{}
+ }
+ }
+
+ return leases, nil
+}
+
+func getARPInfo() (map[string]string, error) {
+ arpInfo := make(map[string]string)
+
+ cmd := exec.Command("arp", "-a")
+ output, err := cmd.Output()
+ if err != nil {
+ return arpInfo, err
+ }
+
+ lines := strings.Split(string(output), "\n")
+ for _, line := range lines {
+ if matches := regexp.MustCompile(`\(([^)]+)\) at ([0-9a-fA-F:]{17})`).FindStringSubmatch(line); len(matches) > 2 {
+ ip := matches[1]
+ mac := matches[2]
+ arpInfo[ip] = mac
+ }
+ }
+
+ return arpInfo, nil
+}
+
+func guessDeviceType(hostname, mac string) string {
+ hostname = strings.ToLower(hostname)
+
+ if strings.Contains(hostname, "iphone") || strings.Contains(hostname, "android") {
+ return "mobile"
+ } else if strings.Contains(hostname, "ipad") || strings.Contains(hostname, "tablet") {
+ return "tablet"
+ } else if strings.Contains(hostname, "macbook") || strings.Contains(hostname, "laptop") {
+ return "laptop"
+ }
+
+ // Deviner par préfixe MAC (OUI)
+ macPrefix := strings.ToUpper(mac[:8])
+ switch macPrefix {
+ case "00:50:56", "00:0C:29", "00:05:69": // VMware
+ return "laptop"
+ case "08:00:27": // VirtualBox
+ return "laptop"
+ default:
+ return "mobile"
+ }
+}
+
+// Fonctions de logging
+
+func addLog(source, message string) {
+ logMutex.Lock()
+ entry := LogEntry{
+ Timestamp: time.Now(),
+ Source: source,
+ Message: message,
+ }
+ logEntries = append(logEntries, entry)
+
+ // Garder seulement les 100 derniers logs
+ if len(logEntries) > 100 {
+ logEntries = logEntries[len(logEntries)-100:]
+ }
+ logMutex.Unlock()
+
+ // Envoyer aux clients WebSocket
+ broadcastToWebSockets(entry)
+
+ // Log vers la console
+ log.Printf("[%s] %s", source, message)
+}
+
+// WebSocket pour les logs en temps réel
+
+func websocketHandler(w http.ResponseWriter, r *http.Request) {
+ conn, err := upgrader.Upgrade(w, r, nil)
+ if err != nil {
+ log.Printf("Erreur WebSocket: %v", err)
+ return
+ }
+ defer conn.Close()
+
+ clientsMutex.Lock()
+ websocketClients[conn] = true
+ clientsMutex.Unlock()
+
+ defer func() {
+ clientsMutex.Lock()
+ delete(websocketClients, conn)
+ clientsMutex.Unlock()
+ }()
+
+ // Envoyer les logs existants
+ logMutex.RLock()
+ for _, entry := range logEntries {
+ conn.WriteJSON(entry)
+ }
+ logMutex.RUnlock()
+
+ // Maintenir la connexion
+ for {
+ _, _, err := conn.ReadMessage()
+ if err != nil {
+ break
+ }
+ }
+}
+
+func broadcastToWebSockets(entry LogEntry) {
+ clientsMutex.RLock()
+ defer clientsMutex.RUnlock()
+
+ for client := range websocketClients {
+ err := client.WriteJSON(entry)
+ if err != nil {
+ client.Close()
+ delete(websocketClients, client)
+ }
+ }
+}
+
+// Tâches périodiques
+
+func periodicStatusUpdate() {
+ ticker := time.NewTicker(5 * time.Second)
+ defer ticker.Stop()
+
+ for range ticker.C {
+ statusMutex.Lock()
+ currentStatus.Connected = isConnected()
+ if !currentStatus.Connected {
+ currentStatus.ConnectedSSID = ""
+ }
+ statusMutex.Unlock()
+ }
+}
+
+func periodicDeviceUpdate() {
+ ticker := time.NewTicker(10 * time.Second)
+ defer ticker.Stop()
+
+ for range ticker.C {
+ devices, err := getConnectedDevices()
+ if err != nil {
+ continue
+ }
+
+ statusMutex.Lock()
+ currentStatus.ConnectedDevices = devices
+ currentStatus.ConnectedCount = len(devices)
+ statusMutex.Unlock()
+ }
+}
diff --git a/oapi-gin.cfg.yaml b/oapi-gin.cfg.yaml
deleted file mode 100644
index b30fef2..0000000
--- a/oapi-gin.cfg.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-package: api
-generate:
- - gin
- - embedded-spec
-output: internal/api/routes.gen.go
diff --git a/oapi-types.cfg.yaml b/oapi-types.cfg.yaml
deleted file mode 100644
index 57b44d2..0000000
--- a/oapi-types.cfg.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
-package: api
-generate:
- - types
-output: internal/api/types.gen.go
diff --git a/openapi.yaml b/openapi.yaml
deleted file mode 100644
index 4357d09..0000000
--- a/openapi.yaml
+++ /dev/null
@@ -1,552 +0,0 @@
-openapi: 3.0.3
-info:
- title: Travel Router Control API
- description: |
- API for controlling a mini travel router with dual WiFi interfaces and Ethernet connectivity.
- The router can operate as a WiFi repeater, connecting to upstream networks while providing
- a hotspot for client devices.
- version: 1.0.0
- contact:
- name: API Support
- license:
- name: MIT
-
-servers:
- - url: http://localhost:8080
- description: Local router API
-
-tags:
- - name: WiFi
- description: WiFi client operations (upstream network connection)
- - name: Hotspot
- description: Access point operations (client-facing hotspot)
- - name: Devices
- description: Connected devices management
- - name: System
- description: System status and monitoring
- - name: Logs
- description: System logs and real-time monitoring
-
-paths:
- /api/wifi/networks:
- get:
- tags:
- - WiFi
- summary: Get discovered WiFi networks
- description: |
- Returns the list of WiFi networks from the last scan without triggering a new scan.
- Returns an empty list if no scan has been performed yet.
- operationId: getWiFiNetworks
- responses:
- '200':
- description: List of discovered networks
- content:
- application/json:
- schema:
- type: array
- items:
- $ref: '#/components/schemas/WiFiNetwork'
- example:
- - ssid: "Hotel-Guest"
- signal: 5
- security: "WPA2"
- channel: 6
- bssid: "aa:bb:cc:dd:ee:ff"
- - ssid: "Public-WiFi"
- signal: 3
- security: "Open"
- channel: 11
- bssid: "11:22:33:44:55:66"
- '500':
- description: Error retrieving networks
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error'
-
- /api/wifi/scan:
- get:
- tags:
- - WiFi
- summary: Scan for available WiFi networks
- description: |
- Triggers a WiFi scan using wpa_supplicant via D-Bus and returns all discovered networks
- sorted by signal strength. The scan takes approximately 2 seconds to complete.
- operationId: scanWiFi
- responses:
- '200':
- description: Successfully scanned networks
- content:
- application/json:
- schema:
- type: array
- items:
- $ref: '#/components/schemas/WiFiNetwork'
- example:
- - ssid: "Hotel-Guest"
- signal: 5
- security: "WPA2"
- channel: 6
- bssid: "aa:bb:cc:dd:ee:ff"
- - ssid: "Public-WiFi"
- signal: 3
- security: "Open"
- channel: 11
- bssid: "11:22:33:44:55:66"
- '500':
- description: Scan error
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error'
-
- /api/wifi/connect:
- post:
- tags:
- - WiFi
- summary: Connect to a WiFi network
- description: |
- Connects the router to an upstream WiFi network using wpa_supplicant.
- Supports both open and password-protected networks (WPA/WPA2).
- operationId: connectWiFi
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/WiFiConnectRequest'
- examples:
- protected:
- summary: WPA2 protected network
- value:
- ssid: "Hotel-Guest"
- password: "guest1234"
- open:
- summary: Open network
- value:
- ssid: "Public-WiFi"
- password: ""
- responses:
- '200':
- description: Successfully connected
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/SuccessResponse'
- '400':
- description: Invalid request data
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error'
- '500':
- description: Connection failed
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error'
-
- /api/wifi/disconnect:
- post:
- tags:
- - WiFi
- summary: Disconnect from WiFi network
- description: |
- Disconnects from the currently connected upstream WiFi network
- and removes all saved network configurations.
- operationId: disconnectWiFi
- responses:
- '200':
- description: Successfully disconnected
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/SuccessResponse'
- '500':
- description: Disconnection failed
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error'
-
- /api/hotspot/toggle:
- post:
- tags:
- - Hotspot
- summary: Toggle hotspot on/off
- description: |
- Enables or disables the hotspot (access point) by starting/stopping
- the hostapd service. Returns the new enabled state and updates
- the system status with current hostapd_cli information.
- operationId: toggleHotspot
- responses:
- '200':
- description: Hotspot state changed successfully
- content:
- application/json:
- schema:
- type: object
- properties:
- enabled:
- type: boolean
- description: Current hotspot state after toggle
- required:
- - enabled
- example:
- enabled: true
- '500':
- description: Failed to change hotspot state
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error'
-
- /api/devices:
- get:
- tags:
- - Devices
- summary: Get connected devices
- description: |
- Returns a list of all devices currently connected to the hotspot.
- Device information is gathered from DHCP leases and ARP tables.
- Only devices with active ARP entries are considered connected.
- operationId: getDevices
- responses:
- '200':
- description: List of connected devices
- content:
- application/json:
- schema:
- type: array
- items:
- $ref: '#/components/schemas/ConnectedDevice'
- example:
- - name: "iPhone-12"
- type: "mobile"
- mac: "aa:bb:cc:11:22:33"
- ip: "192.168.1.100"
- - name: "MacBook-Pro"
- type: "laptop"
- mac: "dd:ee:ff:44:55:66"
- ip: "192.168.1.101"
- '500':
- description: Failed to retrieve device list
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error'
-
- /api/status:
- get:
- tags:
- - System
- summary: Get system status
- description: |
- Returns comprehensive system status including WiFi connection state,
- detailed hotspot status from hostapd_cli, connected device count,
- data usage, and uptime.
- operationId: getStatus
- responses:
- '200':
- description: Current system status
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/SystemStatus'
-
- /api/logs:
- get:
- tags:
- - Logs
- summary: Get system logs
- description: |
- Returns the last 100 log entries from the system.
- For real-time log streaming, use the WebSocket endpoint.
- operationId: getLogs
- responses:
- '200':
- description: List of log entries
- content:
- application/json:
- schema:
- type: array
- items:
- $ref: '#/components/schemas/LogEntry'
- example:
- - timestamp: "2025-10-28T14:32:10Z"
- source: "WiFi"
- message: "Scan terminé - 5 réseaux trouvés"
- - timestamp: "2025-10-28T14:32:15Z"
- source: "WiFi"
- message: "Tentative de connexion à Hotel-Guest"
- delete:
- tags:
- - Logs
- summary: Clear system logs
- description: Clears all stored log entries (keeps only the "logs cleared" entry)
- operationId: clearLogs
- responses:
- '200':
- description: Logs cleared successfully
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/SuccessResponse'
-
- /ws/logs:
- get:
- tags:
- - Logs
- summary: WebSocket for real-time logs
- description: |
- WebSocket endpoint for receiving real-time log updates.
- Upon connection, all existing logs are sent, followed by new logs as they occur.
-
- This is a WebSocket endpoint - upgrade the HTTP connection to WebSocket protocol.
- operationId: logsWebSocket
- responses:
- '101':
- description: WebSocket connection established
- '400':
- description: WebSocket upgrade failed
-
-components:
- schemas:
- WiFiNetwork:
- type: object
- description: Discovered WiFi network information
- properties:
- ssid:
- type: string
- description: Network SSID (name)
- example: "Hotel-Guest"
- signal:
- type: integer
- description: Signal strength (1-5 scale)
- minimum: 1
- maximum: 5
- example: 4
- security:
- type: string
- description: Security type
- enum:
- - Open
- - WEP
- - WPA
- - WPA2
- example: "WPA2"
- channel:
- type: integer
- description: WiFi channel number
- minimum: 1
- maximum: 165
- example: 6
- bssid:
- type: string
- description: Access point MAC address
- pattern: '^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$'
- example: "aa:bb:cc:dd:ee:ff"
- required:
- - ssid
- - signal
- - security
- - channel
- - bssid
-
- WiFiConnectRequest:
- type: object
- description: Request to connect to a WiFi network
- properties:
- ssid:
- type: string
- description: Network SSID to connect to
- example: "Hotel-Guest"
- password:
- type: string
- description: Network password (empty string for open networks)
- example: "guest1234"
- required:
- - ssid
- - password
-
- HotspotStatus:
- type: object
- description: Detailed hotspot status from hostapd_cli
- properties:
- state:
- type: string
- description: Hotspot state (ENABLED, DISABLED, etc.)
- example: "ENABLED"
- ssid:
- type: string
- description: Current SSID being broadcast
- example: "TravelRouter"
- bssid:
- type: string
- description: MAC address of the access point
- pattern: '^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$'
- example: "4a:e3:4e:09:57:f8"
- channel:
- type: integer
- description: Current WiFi channel
- minimum: 1
- maximum: 14
- example: 11
- frequency:
- type: integer
- description: Frequency in MHz
- example: 2462
- numStations:
- type: integer
- description: Number of connected stations
- minimum: 0
- example: 2
- hwMode:
- type: string
- description: Hardware mode (g, a, n, ac, etc.)
- example: "g"
- countryCode:
- type: string
- description: Country code
- example: "VN"
- required:
- - state
-
- ConnectedDevice:
- type: object
- description: Device connected to the hotspot
- properties:
- name:
- type: string
- description: Device hostname
- example: "iPhone-12"
- type:
- type: string
- description: Detected device type
- enum:
- - mobile
- - tablet
- - laptop
- - desktop
- - unknown
- example: "mobile"
- mac:
- type: string
- description: Device MAC address
- pattern: '^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$'
- example: "aa:bb:cc:11:22:33"
- ip:
- type: string
- description: Assigned IP address
- pattern: '^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$'
- example: "192.168.1.100"
- required:
- - name
- - type
- - mac
- - ip
-
- SystemStatus:
- type: object
- description: Overall system status
- properties:
- connected:
- type: boolean
- description: Whether router is connected to upstream WiFi
- example: true
- connectionState:
- type: string
- description: Current WiFi connection state
- enum:
- - connected
- - disconnected
- - connecting
- - disconnecting
- - roaming
- example: "connected"
- connectedSSID:
- type: string
- description: SSID of connected upstream network (empty if not connected)
- example: "Hotel-Guest"
- hotspotStatus:
- allOf:
- - $ref: '#/components/schemas/HotspotStatus'
- nullable: true
- description: Detailed hotspot status (null if hotspot is not running)
- connectedCount:
- type: integer
- description: Number of devices connected to hotspot
- minimum: 0
- example: 3
- dataUsage:
- type: number
- format: double
- description: Total data usage in MB
- example: 145.7
- uptime:
- type: integer
- format: int64
- description: System uptime in seconds
- example: 3600
- connectedDevices:
- type: array
- description: List of devices connected to hotspot
- items:
- $ref: '#/components/schemas/ConnectedDevice'
- required:
- - connected
- - connectionState
- - connectedSSID
- - connectedCount
- - dataUsage
- - uptime
- - connectedDevices
-
- LogEntry:
- type: object
- description: System log entry
- properties:
- timestamp:
- type: string
- format: date-time
- description: When the log entry was created
- example: "2025-10-28T14:32:10Z"
- source:
- type: string
- description: Log source component
- enum:
- - Système
- - WiFi
- - Hotspot
- example: "WiFi"
- message:
- type: string
- description: Log message
- example: "Scan terminé - 5 réseaux trouvés"
- required:
- - timestamp
- - source
- - message
-
- SuccessResponse:
- type: object
- description: Generic success response
- properties:
- status:
- type: string
- enum:
- - success
- example: "success"
- required:
- - status
-
- Error:
- type: object
- description: Error response
- properties:
- error:
- type: string
- description: Error message
- example: "Erreur lors du scan WiFi"
- required:
- - error
diff --git a/static/app.js b/static/app.js
new file mode 100644
index 0000000..6fce809
--- /dev/null
+++ b/static/app.js
@@ -0,0 +1,239 @@
+// État global de l'application
+let appState = {
+ selectedWifi: null,
+ hotspotEnabled: true,
+ connectedDevices: [],
+ wifiNetworks: [],
+ uptime: 0,
+ dataUsage: 0
+};
+
+// Simulation de données
+const mockDevices = [
+ { name: "iPhone 13", type: "mobile", mac: "AA:BB:CC:DD:EE:FF", ip: "192.168.1.101" },
+ { name: "MacBook Pro", type: "laptop", mac: "11:22:33:44:55:66", ip: "192.168.1.102" },
+ { name: "iPad", type: "tablet", mac: "77:88:99:AA:BB:CC", ip: "192.168.1.103" }
+];
+
+// Initialisation
+document.addEventListener('DOMContentLoaded', function() {
+ initializeApp();
+ startPeriodicUpdates();
+});
+
+function initializeApp() {
+ updateWifiList();
+ updateDevicesList();
+ updateStats();
+ addLog("Système", "Interface web initialisée");
+}
+
+async function updateWifiList() {
+ const wifiList = document.getElementById('wifiList');
+ wifiList.innerHTML = '';
+
+ (await (await fetch('/api/wifi/scan')).json()).forEach((network, index) => {
+ const wifiItem = document.createElement('div');
+ wifiItem.className = 'wifi-item';
+ wifiItem.onclick = () => selectWifi(network, wifiItem);
+
+ wifiItem.innerHTML = `
+
+
${network.ssid}
+
${network.security} • Canal ${network.channel}
+
+
+ ${generateSignalBars(network.signal)}
+
+ `;
+
+ wifiList.appendChild(wifiItem);
+ });
+}
+
+function generateSignalBars(strength) {
+ const bars = [];
+ for (let i = 1; i <= 5; i++) {
+ const height = i * 3;
+ const active = i <= strength ? 'active' : '';
+ bars.push(``);
+ }
+ return `${bars.join('')}
`;
+}
+
+function selectWifi(network, element) {
+ // Retirer la sélection précédente
+ document.querySelectorAll('.wifi-item').forEach(item => {
+ item.classList.remove('selected');
+ });
+
+ // Ajouter la sélection
+ element.classList.add('selected');
+ appState.selectedWifi = network;
+
+ addLog("WiFi", `Réseau sélectionné: ${network.ssid}`);
+}
+
+function updateDevicesList() {
+ const devicesList = document.getElementById('devicesList');
+ devicesList.innerHTML = '';
+
+ mockDevices.forEach(device => {
+ const deviceCard = document.createElement('div');
+ deviceCard.className = 'device-card';
+
+ const deviceIcon = getDeviceIcon(device.type);
+
+ deviceCard.innerHTML = `
+ ${deviceIcon}
+ ${device.name}
+ ${device.ip}
+ `;
+
+ devicesList.appendChild(deviceCard);
+ });
+
+ document.getElementById('connectedDevices').textContent = mockDevices.length;
+}
+
+function getDeviceIcon(type) {
+ const icons = {
+ mobile: '',
+ laptop: '',
+ tablet: ''
+ };
+ return icons[type] || icons.mobile;
+}
+
+function updateStats() {
+ appState.uptime += 1;
+ appState.dataUsage += Math.random() * 0.5;
+
+ const hours = Math.floor(appState.uptime / 3600);
+ const minutes = Math.floor((appState.uptime % 3600) / 60);
+ const seconds = appState.uptime % 60;
+
+ document.getElementById('uptime').textContent =
+ `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
+
+ document.getElementById('dataUsage').textContent = `${appState.dataUsage.toFixed(1)} MB`;
+}
+
+function addLog(source, message) {
+ const logContainer = document.getElementById('logContainer');
+ const timestamp = new Date().toLocaleTimeString();
+ const logEntry = document.createElement('div');
+ logEntry.className = 'log-entry';
+ logEntry.innerHTML = `[${timestamp}] ${source}: ${message}`;
+
+ logContainer.appendChild(logEntry);
+ logContainer.scrollTop = logContainer.scrollHeight;
+}
+
+function showNotification(message, type = 'success') {
+ const notification = document.getElementById('notification');
+ notification.textContent = message;
+ notification.className = `notification ${type}`;
+ notification.classList.add('show');
+
+ setTimeout(() => {
+ notification.classList.remove('show');
+ }, 3000);
+}
+
+// Fonctions d'action
+function scanWifi() {
+ const scanBtn = document.getElementById('scanBtn');
+ const originalText = scanBtn.textContent;
+
+ scanBtn.innerHTML = ' Scan en cours...';
+
+ setTimeout(() => {
+ updateWifiList();
+ scanBtn.textContent = originalText;
+ showNotification('Scan terminé - Réseaux mis à jour');
+ addLog("WiFi", "Scan des réseaux terminé");
+ }, 2000);
+}
+
+function connectToWifi() {
+ if (!appState.selectedWifi) {
+ showNotification('Veuillez sélectionner un réseau WiFi', 'error');
+ return;
+ }
+
+ const password = document.getElementById('wifiPassword').value;
+ if (!password && appState.selectedWifi.security !== 'Open') {
+ showNotification('Mot de passe requis', 'error');
+ return;
+ }
+
+ const connectBtn = document.getElementById('connectBtn');
+ const originalText = connectBtn.textContent;
+
+ connectBtn.innerHTML = ' Connexion...';
+
+ setTimeout(() => {
+ connectBtn.textContent = originalText;
+ showNotification(`Connecté à ${appState.selectedWifi.ssid}`);
+ addLog("WiFi", `Connexion établie avec ${appState.selectedWifi.ssid}`);
+
+ // Mettre à jour le statut
+ document.getElementById('connectionStatus').innerHTML = `
+
+ Connecté à ${appState.selectedWifi.ssid}
+ `;
+ }, 3000);
+}
+
+function updateHotspot() {
+ const name = document.getElementById('hotspotName').value;
+ const password = document.getElementById('hotspotPassword').value;
+ const channel = document.getElementById('hotspotChannel').value;
+
+ if (!name || !password) {
+ showNotification('Nom et mot de passe requis', 'error');
+ return;
+ }
+
+ showNotification('Configuration du hotspot mise à jour');
+ addLog("Hotspot", `Configuration mise à jour: ${name} (Canal ${channel})`);
+}
+
+function toggleHotspot() {
+ appState.hotspotEnabled = !appState.hotspotEnabled;
+ const btn = document.getElementById('hotspotBtn');
+
+ if (appState.hotspotEnabled) {
+ btn.textContent = 'Arrêter le hotspot';
+ showNotification('Hotspot activé');
+ addLog("Hotspot", "Hotspot activé");
+ } else {
+ btn.textContent = 'Démarrer le hotspot';
+ showNotification('Hotspot désactivé');
+ addLog("Hotspot", "Hotspot désactivé");
+ }
+}
+
+function clearLogs() {
+ document.getElementById('logContainer').innerHTML = '';
+ addLog("Système", "Logs effacés");
+}
+
+// Mises à jour périodiques
+function startPeriodicUpdates() {
+ setInterval(updateStats, 1000);
+ setInterval(() => {
+ // Simulation de nouveaux logs
+ if (Math.random() > 0.95) {
+ const events = [
+ "Nouveau client connecté",
+ "Paquet routé vers l'extérieur",
+ "Vérification de la connexion",
+ "Mise à jour des tables de routage"
+ ];
+ const randomEvent = events[Math.floor(Math.random() * events.length)];
+ addLog("Système", randomEvent);
+ }
+ }, 5000);
+}
diff --git a/static/index.html b/static/index.html
new file mode 100644
index 0000000..dbb7131
--- /dev/null
+++ b/static/index.html
@@ -0,0 +1,135 @@
+
+
+
+
+
+ WiFi Repeater Control
+
+
+
+
+
+
+
+
+
0
+
Appareils connectés
+
+
+
0 MB
+
Données utilisées
+
+
+
00:00:00
+
Temps de fonctionnement
+
+
+
+
+
+
+
+ Connexion WiFi Externe
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Configuration Hotspot
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Appareils connectés
+
+
+
+
+
+
+
+
+
+
+ Logs système
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/static/style.css b/static/style.css
new file mode 100644
index 0000000..3ea544a
--- /dev/null
+++ b/static/style.css
@@ -0,0 +1,338 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ min-height: 100vh;
+ padding: 20px;
+}
+
+.container {
+ max-width: 1200px;
+ margin: 0 auto;
+ background: rgba(255, 255, 255, 0.95);
+ backdrop-filter: blur(10px);
+ border-radius: 20px;
+ padding: 30px;
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
+}
+
+.header {
+ text-align: center;
+ margin-bottom: 40px;
+ padding-bottom: 20px;
+ border-bottom: 2px solid #e0e0e0;
+}
+
+.header h1 {
+ color: #333;
+ font-size: 2.5em;
+ margin-bottom: 10px;
+}
+
+.status-indicator {
+ display: inline-flex;
+ align-items: center;
+ gap: 10px;
+ padding: 10px 20px;
+ border-radius: 25px;
+ font-weight: 500;
+ margin-top: 10px;
+}
+
+.status-online {
+ background: #d4edda;
+ color: #155724;
+}
+
+.status-offline {
+ background: #f8d7da;
+ color: #721c24;
+}
+
+.status-dot {
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ background: currentColor;
+ animation: pulse 2s infinite;
+}
+
+@keyframes pulse {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.5; }
+}
+
+.grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
+ gap: 30px;
+ margin-bottom: 30px;
+}
+
+.card {
+ background: white;
+ border-radius: 15px;
+ padding: 25px;
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
+}
+
+.card:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
+}
+
+.card h2 {
+ color: #333;
+ margin-bottom: 20px;
+ font-size: 1.4em;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.icon {
+ width: 24px;
+ height: 24px;
+ fill: currentColor;
+}
+
+.form-group {
+ margin-bottom: 20px;
+}
+
+.form-group label {
+ display: block;
+ margin-bottom: 8px;
+ color: #555;
+ font-weight: 500;
+}
+
+.form-group input, .form-group select {
+ width: 100%;
+ padding: 12px 16px;
+ border: 2px solid #e0e0e0;
+ border-radius: 8px;
+ font-size: 16px;
+ transition: border-color 0.3s ease;
+}
+
+.form-group input:focus, .form-group select:focus {
+ outline: none;
+ border-color: #667eea;
+}
+
+.btn {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ border: none;
+ padding: 12px 24px;
+ border-radius: 8px;
+ font-size: 16px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
+ width: 100%;
+}
+
+.btn:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
+}
+
+.btn:active {
+ transform: translateY(0);
+}
+
+.btn-secondary {
+ background: #6c757d;
+ margin-top: 10px;
+}
+
+.btn-danger {
+ background: #dc3545;
+}
+
+.wifi-list {
+ max-height: 300px;
+ overflow-y: auto;
+ border: 1px solid #e0e0e0;
+ border-radius: 8px;
+ margin-bottom: 20px;
+}
+
+.wifi-item {
+ padding: 12px 16px;
+ border-bottom: 1px solid #f0f0f0;
+ cursor: pointer;
+ transition: background-color 0.2s ease;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.wifi-item:hover {
+ background-color: #f8f9fa;
+}
+
+.wifi-item:last-child {
+ border-bottom: none;
+}
+
+.wifi-item.selected {
+ background-color: #e7f3ff;
+ border-left: 4px solid #667eea;
+}
+
+.wifi-signal {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.signal-strength {
+ width: 20px;
+ height: 20px;
+ position: relative;
+}
+
+.signal-bars {
+ display: flex;
+ gap: 2px;
+ align-items: flex-end;
+}
+
+.signal-bar {
+ width: 3px;
+ background: #ccc;
+ border-radius: 1px;
+}
+
+.signal-bar.active {
+ background: #28a745;
+}
+
+.devices-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 15px;
+}
+
+.device-card {
+ background: #f8f9fa;
+ border-radius: 10px;
+ padding: 15px;
+ text-align: center;
+ transition: transform 0.2s ease;
+}
+
+.device-card:hover {
+ transform: scale(1.05);
+}
+
+.device-icon {
+ width: 40px;
+ height: 40px;
+ margin: 0 auto 10px;
+ fill: #667eea;
+}
+
+.log-container {
+ background: #1a1a1a;
+ color: #00ff00;
+ padding: 20px;
+ border-radius: 10px;
+ font-family: 'Courier New', monospace;
+ font-size: 14px;
+ max-height: 300px;
+ overflow-y: auto;
+ line-height: 1.4;
+}
+
+.log-entry {
+ margin-bottom: 5px;
+}
+
+.log-timestamp {
+ color: #888;
+}
+
+.stats-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
+ gap: 20px;
+}
+
+.stat-card {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ padding: 20px;
+ border-radius: 10px;
+ text-align: center;
+}
+
+.stat-value {
+ font-size: 2em;
+ font-weight: bold;
+ margin-bottom: 5px;
+}
+
+.stat-label {
+ font-size: 0.9em;
+ opacity: 0.9;
+}
+
+.loading {
+ display: inline-block;
+ width: 20px;
+ height: 20px;
+ border: 3px solid #f3f3f3;
+ border-top: 3px solid #667eea;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+.notification {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ padding: 15px 20px;
+ border-radius: 10px;
+ color: white;
+ font-weight: 500;
+ z-index: 1000;
+ transform: translateX(100%);
+ transition: transform 0.3s ease;
+}
+
+.notification.show {
+ transform: translateX(0);
+}
+
+.notification.success {
+ background: #28a745;
+}
+
+.notification.error {
+ background: #dc3545;
+}
+
+@media (max-width: 768px) {
+ .grid {
+ grid-template-columns: 1fr;
+ }
+
+ .container {
+ padding: 20px;
+ }
+}