Report hotspot config

This commit is contained in:
nemunaire 2026-01-01 17:32:46 +07:00
commit c443fce24f
8 changed files with 244 additions and 160 deletions

View file

@ -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() { async function toggleHotspot() {
const toggle = document.getElementById('hotspotToggle'); const toggle = document.getElementById('hotspotToggle');
const enabled = toggle.checked; const enabled = toggle.checked;
@ -286,9 +249,12 @@ function updateStatusDisplay(status) {
const hotspotText = hotspotStatus.querySelector('.status-text'); const hotspotText = hotspotStatus.querySelector('.status-text');
const hotspotToggle = document.getElementById('hotspotToggle'); const hotspotToggle = document.getElementById('hotspotToggle');
if (status.hotspotEnabled) { const isHotspotEnabled = status.hotspotStatus && status.hotspotStatus.state === 'ENABLED';
if (isHotspotEnabled) {
hotspotDot.className = 'status-dot active'; 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; hotspotToggle.checked = true;
} else { } else {
hotspotDot.className = 'status-dot offline'; hotspotDot.className = 'status-dot offline';
@ -296,7 +262,10 @@ function updateStatusDisplay(status) {
hotspotToggle.checked = false; 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 // Check if connectedSSID changed and refresh WiFi list if needed
const connectedSSIDChanged = appState.connectedSSID !== status.connectedSSID; const connectedSSIDChanged = appState.connectedSSID !== status.connectedSSID;
@ -409,11 +378,21 @@ function addLogEntry(log) {
const timestamp = new Date(log.timestamp).toLocaleTimeString('fr-FR'); const timestamp = new Date(log.timestamp).toLocaleTimeString('fr-FR');
logEntry.innerHTML = ` const timestampSpan = document.createElement('span');
<span class="log-timestamp">${timestamp}</span> timestampSpan.className = 'log-timestamp';
<span class="log-source">[${escapeHtml(log.source)}]</span> timestampSpan.textContent = timestamp;
<span class="log-message">${escapeHtml(log.message)}</span>
`; 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); 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 ===== // ===== WebSocket Functions =====
function connectWebSocket() { function connectWebSocket() {

View file

@ -133,7 +133,7 @@
<svg class="icon" viewBox="0 0 24 24" width="20" height="20"> <svg class="icon" viewBox="0 0 24 24" width="20" height="20">
<path fill="currentColor" d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4M12,6A6,6 0 0,0 6,12A6,6 0 0,0 12,18A6,6 0 0,0 18,12A6,6 0 0,0 12,6M12,8A4,4 0 0,1 16,12A4,4 0 0,1 12,16A4,4 0 0,1 8,12A4,4 0 0,1 12,8Z"/> <path fill="currentColor" d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4M12,6A6,6 0 0,0 6,12A6,6 0 0,0 12,18A6,6 0 0,0 18,12A6,6 0 0,0 12,6M12,8A4,4 0 0,1 16,12A4,4 0 0,1 12,16A4,4 0 0,1 8,12A4,4 0 0,1 12,8Z"/>
</svg> </svg>
Configuration Hotspot Hotspot Status
</h2> </h2>
<div class="toggle-switch"> <div class="toggle-switch">
<input type="checkbox" id="hotspotToggle" checked onchange="toggleHotspot()"> <input type="checkbox" id="hotspotToggle" checked onchange="toggleHotspot()">
@ -141,54 +141,12 @@
</div> </div>
</div> </div>
<div class="form-group"> <div class="hotspot-details" id="hotspotDetails">
<label for="hotspotName"> <div class="hotspot-info-item">
<svg class="input-icon" viewBox="0 0 24 24" width="16" height="16"> <span class="info-label">État:</span>
<path fill="currentColor" d="M12,6A10,10 0 0,0 2,16C2,16.5 2.04,17 2.11,17.5L6,13.61C6,13.61 9.26,15.45 9.26,15.45L11.12,13C11.12,13 12.19,13.53 12.19,13.53C12.19,13.53 13.72,12 13.72,12L12,10.28C12,10.28 14.97,8.12 14.97,8.12L22,15.15C22,14.77 22,14.39 22,14A10,10 0 0,0 12,6Z"/> <span class="info-value">Chargement...</span>
</svg> </div>
Nom du réseau (SSID)
</label>
<input type="text" id="hotspotName" value="TravelRouter" placeholder="Nom du hotspot" maxlength="32">
</div> </div>
<div class="form-group">
<label for="hotspotPassword">
<svg class="input-icon" viewBox="0 0 24 24" width="16" height="16">
<path fill="currentColor" d="M12,17A2,2 0 0,0 14,15C14,13.89 13.1,13 12,13A2,2 0 0,0 10,15A2,2 0 0,0 12,17M18,8A2,2 0 0,1 20,10V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V10C4,8.89 4.9,8 6,8H7V6A5,5 0 0,1 12,1A5,5 0 0,1 17,6V8H18M12,3A3,3 0 0,0 9,6V8H15V6A3,3 0 0,0 12,3Z"/>
</svg>
Mot de passe (8-63 caractères)
</label>
<input type="password" id="hotspotPassword" value="travel123" placeholder="Mot de passe du hotspot" minlength="8" maxlength="63">
</div>
<div class="form-group">
<label for="hotspotChannel">
<svg class="input-icon" viewBox="0 0 24 24" width="16" height="16">
<path fill="currentColor" d="M9,10H7V17H9V10M13,10H11V17H13V10M17,10H15V17H17V10M19,3H18V1H16V3H8V1H6V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3M19,19H5V8H19V19Z"/>
</svg>
Canal WiFi (2.4GHz)
</label>
<select id="hotspotChannel">
<option value="1">Canal 1 (2412 MHz)</option>
<option value="2">Canal 2 (2417 MHz)</option>
<option value="3">Canal 3 (2422 MHz)</option>
<option value="4">Canal 4 (2427 MHz)</option>
<option value="5">Canal 5 (2432 MHz)</option>
<option value="6" selected>Canal 6 (2437 MHz)</option>
<option value="7">Canal 7 (2442 MHz)</option>
<option value="8">Canal 8 (2447 MHz)</option>
<option value="9">Canal 9 (2452 MHz)</option>
<option value="10">Canal 10 (2457 MHz)</option>
<option value="11">Canal 11 (2462 MHz)</option>
</select>
</div>
<button class="btn btn-primary" onclick="updateHotspot()">
<svg viewBox="0 0 24 24" width="18" height="18">
<path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"/>
</svg>
Mettre à jour la configuration
</button>
</div> </div>
</div> </div>

View file

@ -747,6 +747,39 @@ body {
background: var(--text-secondary); 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 */ /* Responsive Design */
@media (max-width: 1024px) { @media (max-width: 1024px) {
.grid { .grid {

View file

@ -82,11 +82,11 @@ func ConfigureHotspot(c *gin.Context) {
// ToggleHotspot handles hotspot enable/disable // ToggleHotspot handles hotspot enable/disable
func ToggleHotspot(c *gin.Context, status *models.SystemStatus) { func ToggleHotspot(c *gin.Context, status *models.SystemStatus) {
status.HotspotEnabled = !status.HotspotEnabled // Determine current state
enabled := status.HotspotEnabled isEnabled := status.HotspotStatus != nil && status.HotspotStatus.State == "ENABLED"
var err error var err error
if enabled { if !isEnabled {
err = hotspot.Start() err = hotspot.Start()
logging.AddLog("Hotspot", "Hotspot activé") logging.AddLog("Hotspot", "Hotspot activé")
} else { } else {
@ -100,7 +100,10 @@ func ToggleHotspot(c *gin.Context, status *models.SystemStatus) {
return 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 // GetDevices returns connected devices

View file

@ -12,6 +12,7 @@ import (
"github.com/nemunaire/repeater/internal/api" "github.com/nemunaire/repeater/internal/api"
"github.com/nemunaire/repeater/internal/config" "github.com/nemunaire/repeater/internal/config"
"github.com/nemunaire/repeater/internal/device" "github.com/nemunaire/repeater/internal/device"
"github.com/nemunaire/repeater/internal/hotspot"
"github.com/nemunaire/repeater/internal/logging" "github.com/nemunaire/repeater/internal/logging"
"github.com/nemunaire/repeater/internal/models" "github.com/nemunaire/repeater/internal/models"
"github.com/nemunaire/repeater/internal/wifi" "github.com/nemunaire/repeater/internal/wifi"
@ -32,7 +33,7 @@ func New(assets embed.FS) *App {
Status: models.SystemStatus{ Status: models.SystemStatus{
Connected: false, Connected: false,
ConnectedSSID: "", ConnectedSSID: "",
HotspotEnabled: true, HotspotStatus: nil,
ConnectedCount: 0, ConnectedCount: 0,
DataUsage: 0.0, DataUsage: 0.0,
Uptime: 0, Uptime: 0,
@ -131,6 +132,9 @@ func (a *App) periodicStatusUpdate() {
a.Status.ConnectedSSID = wifi.GetConnectedSSID() a.Status.ConnectedSSID = wifi.GetConnectedSSID()
a.Status.Uptime = getSystemUptime() a.Status.Uptime = getSystemUptime()
// Get detailed hotspot status
a.Status.HotspotStatus = hotspot.GetDetailedStatus()
// Get network data usage for WiFi interface // Get network data usage for WiFi interface
if a.Config != nil { if a.Config != nil {
rxBytes, txBytes := getInterfaceBytes(a.Config.WifiInterface) rxBytes, txBytes := getInterfaceBytes(a.Config.WifiInterface)

View file

@ -4,6 +4,8 @@ import (
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"strconv"
"strings"
"github.com/nemunaire/repeater/internal/models" "github.com/nemunaire/repeater/internal/models"
) )
@ -36,12 +38,74 @@ rsn_pairwise=CCMP
// Start starts the hotspot // Start starts the hotspot
func Start() error { func Start() error {
cmd := exec.Command("systemctl", "start", "hostapd") cmd := exec.Command("/etc/init.d/hostapd", "start")
return cmd.Run() return cmd.Run()
} }
// Stop stops the hotspot // Stop stops the hotspot
func Stop() error { func Stop() error {
cmd := exec.Command("systemctl", "stop", "hostapd") cmd := exec.Command("/etc/init.d/hostapd", "stop")
return cmd.Run() 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
}

View file

@ -26,11 +26,23 @@ type HotspotConfig struct {
Channel int `json:"channel"` 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 // SystemStatus represents overall system status
type SystemStatus struct { type SystemStatus struct {
Connected bool `json:"connected"` Connected bool `json:"connected"`
ConnectedSSID string `json:"connectedSSID"` ConnectedSSID string `json:"connectedSSID"`
HotspotEnabled bool `json:"hotspotEnabled"` HotspotStatus *HotspotStatus `json:"hotspotStatus,omitempty"` // Detailed hotspot status
ConnectedCount int `json:"connectedCount"` ConnectedCount int `json:"connectedCount"`
DataUsage float64 `json:"dataUsage"` DataUsage float64 `json:"dataUsage"`
Uptime int64 `json:"uptime"` Uptime int64 `json:"uptime"`

View file

@ -133,42 +133,6 @@ paths:
schema: schema:
$ref: '#/components/schemas/Error' $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: /api/hotspot/toggle:
post: post:
tags: tags:
@ -176,7 +140,8 @@ paths:
summary: Toggle hotspot on/off summary: Toggle hotspot on/off
description: | description: |
Enables or disables the hotspot (access point) by starting/stopping 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 operationId: toggleHotspot
responses: responses:
'200': '200':
@ -242,7 +207,8 @@ paths:
summary: Get system status summary: Get system status
description: | description: |
Returns comprehensive system status including WiFi connection state, 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 operationId: getStatus
responses: responses:
'200': '200':
@ -367,32 +333,48 @@ components:
- ssid - ssid
- password - password
HotspotConfig: HotspotStatus:
type: object type: object
description: Hotspot (access point) configuration description: Detailed hotspot status from hostapd_cli
properties: properties:
state:
type: string
description: Hotspot state (ENABLED, DISABLED, etc.)
example: "ENABLED"
ssid: ssid:
type: string type: string
description: Hotspot SSID (network name) description: Current SSID being broadcast
minLength: 1
maxLength: 32
example: "TravelRouter" example: "TravelRouter"
password: bssid:
type: string type: string
description: WPA2 password (minimum 8 characters) description: MAC address of the access point
minLength: 8 pattern: '^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$'
maxLength: 63 example: "4a:e3:4e:09:57:f8"
example: "secure123"
channel: channel:
type: integer type: integer
description: WiFi channel (1-11 for 2.4GHz) description: Current WiFi channel
minimum: 1 minimum: 1
maximum: 14 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: required:
- ssid - state
- password
- channel
ConnectedDevice: ConnectedDevice:
type: object type: object
@ -440,10 +422,11 @@ components:
type: string type: string
description: SSID of connected upstream network (empty if not connected) description: SSID of connected upstream network (empty if not connected)
example: "Hotel-Guest" example: "Hotel-Guest"
hotspotEnabled: hotspotStatus:
type: boolean allOf:
description: Whether the hotspot is currently enabled - $ref: '#/components/schemas/HotspotStatus'
example: true nullable: true
description: Detailed hotspot status (null if hotspot is not running)
connectedCount: connectedCount:
type: integer type: integer
description: Number of devices connected to hotspot description: Number of devices connected to hotspot
@ -452,7 +435,7 @@ components:
dataUsage: dataUsage:
type: number type: number
format: double format: double
description: Total data usage in MB (placeholder for future implementation) description: Total data usage in MB
example: 145.7 example: 145.7
uptime: uptime:
type: integer type: integer
@ -467,7 +450,6 @@ components:
required: required:
- connected - connected
- connectedSSID - connectedSSID
- hotspotEnabled
- connectedCount - connectedCount
- dataUsage - dataUsage
- uptime - uptime