diff --git a/cmd/repeater/static/app.js b/cmd/repeater/static/app.js index 3813ab8..eacedee 100644 --- a/cmd/repeater/static/app.js +++ b/cmd/repeater/static/app.js @@ -149,43 +149,6 @@ async function disconnectWifi() { } } -async function updateHotspot() { - const ssid = document.getElementById('hotspotName').value; - const password = document.getElementById('hotspotPassword').value; - const channel = parseInt(document.getElementById('hotspotChannel').value); - - if (!ssid || ssid.length > 32) { - showToast('warning', 'Attention', 'SSID invalide (1-32 caractères)'); - return; - } - - if (!password || password.length < 8 || password.length > 63) { - showToast('warning', 'Attention', 'Mot de passe invalide (8-63 caractères)'); - return; - } - - showLoading(true); - - try { - const response = await fetch('/api/hotspot/config', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ ssid, password, channel }) - }); - - if (response.ok) { - showToast('success', 'Configuration', 'Hotspot configuré avec succès'); - } else { - throw new Error('Configuration failed'); - } - } catch (error) { - console.error('Error configuring hotspot:', error); - showToast('error', 'Erreur', 'Échec de la configuration'); - } finally { - showLoading(false); - } -} - async function toggleHotspot() { const toggle = document.getElementById('hotspotToggle'); const enabled = toggle.checked; @@ -286,9 +249,12 @@ function updateStatusDisplay(status) { const hotspotText = hotspotStatus.querySelector('.status-text'); const hotspotToggle = document.getElementById('hotspotToggle'); - if (status.hotspotEnabled) { + const isHotspotEnabled = status.hotspotStatus && status.hotspotStatus.state === 'ENABLED'; + + if (isHotspotEnabled) { hotspotDot.className = 'status-dot active'; - hotspotText.textContent = 'Hotspot actif'; + const numStations = status.hotspotStatus.numStations || 0; + hotspotText.textContent = `Hotspot actif (${numStations} client${numStations > 1 ? 's' : ''})`; hotspotToggle.checked = true; } else { hotspotDot.className = 'status-dot offline'; @@ -296,7 +262,10 @@ function updateStatusDisplay(status) { hotspotToggle.checked = false; } - appState.hotspotEnabled = status.hotspotEnabled; + appState.hotspotEnabled = isHotspotEnabled; + + // Update hotspot details if available + updateHotspotDetails(status.hotspotStatus); // Check if connectedSSID changed and refresh WiFi list if needed const connectedSSIDChanged = appState.connectedSSID !== status.connectedSSID; @@ -409,11 +378,21 @@ function addLogEntry(log) { const timestamp = new Date(log.timestamp).toLocaleTimeString('fr-FR'); - logEntry.innerHTML = ` - ${timestamp} - [${escapeHtml(log.source)}] - ${escapeHtml(log.message)} - `; + 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); @@ -422,6 +401,55 @@ function addLogEntry(log) { } } +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() { diff --git a/cmd/repeater/static/index.html b/cmd/repeater/static/index.html index 90e4be4..d53a645 100644 --- a/cmd/repeater/static/index.html +++ b/cmd/repeater/static/index.html @@ -133,7 +133,7 @@ - Configuration Hotspot + Hotspot Status
@@ -141,54 +141,12 @@
-
- - +
+
+ État: + Chargement... +
- -
- - -
- -
- - -
- -
diff --git a/cmd/repeater/static/style.css b/cmd/repeater/static/style.css index e2de87c..26464cd 100644 --- a/cmd/repeater/static/style.css +++ b/cmd/repeater/static/style.css @@ -747,6 +747,39 @@ body { 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 { diff --git a/internal/api/handlers/handlers.go b/internal/api/handlers/handlers.go index 4bb2302..fefa8ef 100644 --- a/internal/api/handlers/handlers.go +++ b/internal/api/handlers/handlers.go @@ -82,11 +82,11 @@ func ConfigureHotspot(c *gin.Context) { // ToggleHotspot handles hotspot enable/disable func ToggleHotspot(c *gin.Context, status *models.SystemStatus) { - status.HotspotEnabled = !status.HotspotEnabled - enabled := status.HotspotEnabled + // Determine current state + isEnabled := status.HotspotStatus != nil && status.HotspotStatus.State == "ENABLED" var err error - if enabled { + if !isEnabled { err = hotspot.Start() logging.AddLog("Hotspot", "Hotspot activé") } else { @@ -100,7 +100,10 @@ func ToggleHotspot(c *gin.Context, status *models.SystemStatus) { return } - c.JSON(http.StatusOK, gin.H{"enabled": enabled}) + // Update status immediately + status.HotspotStatus = hotspot.GetDetailedStatus() + + c.JSON(http.StatusOK, gin.H{"enabled": !isEnabled}) } // GetDevices returns connected devices diff --git a/internal/app/app.go b/internal/app/app.go index 3924f2a..5cbf5f5 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -12,6 +12,7 @@ import ( "github.com/nemunaire/repeater/internal/api" "github.com/nemunaire/repeater/internal/config" "github.com/nemunaire/repeater/internal/device" + "github.com/nemunaire/repeater/internal/hotspot" "github.com/nemunaire/repeater/internal/logging" "github.com/nemunaire/repeater/internal/models" "github.com/nemunaire/repeater/internal/wifi" @@ -32,7 +33,7 @@ func New(assets embed.FS) *App { Status: models.SystemStatus{ Connected: false, ConnectedSSID: "", - HotspotEnabled: true, + HotspotStatus: nil, ConnectedCount: 0, DataUsage: 0.0, Uptime: 0, @@ -131,6 +132,9 @@ func (a *App) periodicStatusUpdate() { 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) diff --git a/internal/hotspot/hotspot.go b/internal/hotspot/hotspot.go index aff0483..793352f 100644 --- a/internal/hotspot/hotspot.go +++ b/internal/hotspot/hotspot.go @@ -4,6 +4,8 @@ import ( "fmt" "os" "os/exec" + "strconv" + "strings" "github.com/nemunaire/repeater/internal/models" ) @@ -36,12 +38,74 @@ rsn_pairwise=CCMP // Start starts the hotspot func Start() error { - cmd := exec.Command("systemctl", "start", "hostapd") + cmd := exec.Command("/etc/init.d/hostapd", "start") return cmd.Run() } // Stop stops the hotspot func Stop() error { - cmd := exec.Command("systemctl", "stop", "hostapd") + 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/models/models.go b/internal/models/models.go index d582364..b1500d1 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -26,11 +26,23 @@ type HotspotConfig struct { 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"` ConnectedSSID string `json:"connectedSSID"` - HotspotEnabled bool `json:"hotspotEnabled"` + HotspotStatus *HotspotStatus `json:"hotspotStatus,omitempty"` // Detailed hotspot status ConnectedCount int `json:"connectedCount"` DataUsage float64 `json:"dataUsage"` Uptime int64 `json:"uptime"` diff --git a/openapi.yaml b/openapi.yaml index d1ee08e..bdcf204 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -133,42 +133,6 @@ paths: schema: $ref: '#/components/schemas/Error' - /api/hotspot/config: - post: - tags: - - Hotspot - summary: Configure hotspot settings - description: | - Updates the hotspot (access point) configuration including SSID, password, - and WiFi channel. Changes are written to hostapd configuration file. - The hotspot needs to be restarted for changes to take effect. - operationId: configureHotspot - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/HotspotConfig' - responses: - '200': - description: Configuration updated successfully - content: - application/json: - schema: - $ref: '#/components/schemas/SuccessResponse' - '400': - description: Invalid configuration data - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - '500': - description: Configuration update failed - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - /api/hotspot/toggle: post: tags: @@ -176,7 +140,8 @@ paths: summary: Toggle hotspot on/off description: | Enables or disables the hotspot (access point) by starting/stopping - the hostapd service. Returns the new enabled state. + the hostapd service. Returns the new enabled state and updates + the system status with current hostapd_cli information. operationId: toggleHotspot responses: '200': @@ -242,7 +207,8 @@ paths: summary: Get system status description: | Returns comprehensive system status including WiFi connection state, - hotspot status, connected device count, data usage, and uptime. + detailed hotspot status from hostapd_cli, connected device count, + data usage, and uptime. operationId: getStatus responses: '200': @@ -367,32 +333,48 @@ components: - ssid - password - HotspotConfig: + HotspotStatus: type: object - description: Hotspot (access point) configuration + description: Detailed hotspot status from hostapd_cli properties: + state: + type: string + description: Hotspot state (ENABLED, DISABLED, etc.) + example: "ENABLED" ssid: type: string - description: Hotspot SSID (network name) - minLength: 1 - maxLength: 32 + description: Current SSID being broadcast example: "TravelRouter" - password: + bssid: type: string - description: WPA2 password (minimum 8 characters) - minLength: 8 - maxLength: 63 - example: "secure123" + 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: WiFi channel (1-11 for 2.4GHz) + description: Current WiFi channel minimum: 1 maximum: 14 - example: 6 + 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: - - ssid - - password - - channel + - state ConnectedDevice: type: object @@ -440,10 +422,11 @@ components: type: string description: SSID of connected upstream network (empty if not connected) example: "Hotel-Guest" - hotspotEnabled: - type: boolean - description: Whether the hotspot is currently enabled - example: true + 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 @@ -452,7 +435,7 @@ components: dataUsage: type: number format: double - description: Total data usage in MB (placeholder for future implementation) + description: Total data usage in MB example: 145.7 uptime: type: integer @@ -467,7 +450,6 @@ components: required: - connected - connectedSSID - - hotspotEnabled - connectedCount - dataUsage - uptime