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 @@
-
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