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() {
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 = `
<span class="log-timestamp">${timestamp}</span>
<span class="log-source">[${escapeHtml(log.source)}]</span>
<span class="log-message">${escapeHtml(log.message)}</span>
`;
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() {

View file

@ -133,7 +133,7 @@
<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"/>
</svg>
Configuration Hotspot
Hotspot Status
</h2>
<div class="toggle-switch">
<input type="checkbox" id="hotspotToggle" checked onchange="toggleHotspot()">
@ -141,54 +141,12 @@
</div>
</div>
<div class="form-group">
<label for="hotspotName">
<svg class="input-icon" viewBox="0 0 24 24" width="16" height="16">
<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"/>
</svg>
Nom du réseau (SSID)
</label>
<input type="text" id="hotspotName" value="TravelRouter" placeholder="Nom du hotspot" maxlength="32">
<div class="hotspot-details" id="hotspotDetails">
<div class="hotspot-info-item">
<span class="info-label">État:</span>
<span class="info-value">Chargement...</span>
</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>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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