diff --git a/cmd/repeater/static/app.js b/cmd/repeater/static/app.js
index a7c367d..7d8a1b4 100644
--- a/cmd/repeater/static/app.js
+++ b/cmd/repeater/static/app.js
@@ -401,13 +401,17 @@ function displayDevices(devices) {
const deviceCard = document.createElement('div');
deviceCard.className = 'device-card';
+ const displayName = device.name && device.name.trim() !== ''
+ ? device.name
+ : 'Sans nom';
+
deviceCard.innerHTML = `
${getDeviceIcon(device.type)}
-
${escapeHtml(device.name)}
- ${device.type}
+ ${escapeHtml(displayName)}
+ ${escapeHtml(formatDeviceType(device.type))}
-
${device.ip}
-
${device.mac}
+
${escapeHtml(device.ip || '—')}
+
${escapeHtml(device.mac)}
`;
@@ -415,6 +419,17 @@ function displayDevices(devices) {
});
}
+function formatDeviceType(type) {
+ const labels = {
+ mobile: 'Mobile',
+ tablet: 'Tablette',
+ laptop: 'Portable',
+ desktop: 'Ordinateur',
+ unknown: 'Inconnu'
+ };
+ return labels[type] || labels.unknown;
+}
+
function addLogEntry(log) {
const logContainer = document.getElementById('logContainer');
diff --git a/internal/station/backend/types.go b/internal/station/backend/types.go
index f4900ef..e58dd74 100644
--- a/internal/station/backend/types.go
+++ b/internal/station/backend/types.go
@@ -87,19 +87,26 @@ type BackendConfig struct {
HostapdInterface string // Hostapd interface name for DBus
}
-// GuessDeviceType attempts to guess device type from hostname and MAC address
+// GuessDeviceType attempts to guess device type from hostname and MAC address.
+// Returns "unknown" when no signal is strong enough — the frontend renders that
+// as a neutral icon rather than mislabeling unknown devices as "mobile".
func GuessDeviceType(hostname, mac string) string {
hostname = strings.ToLower(hostname)
- if strings.Contains(hostname, "iphone") || strings.Contains(hostname, "android") {
+ if strings.Contains(hostname, "iphone") || strings.Contains(hostname, "android") ||
+ strings.Contains(hostname, "pixel") || strings.Contains(hostname, "galaxy") {
return "mobile"
} else if strings.Contains(hostname, "ipad") || strings.Contains(hostname, "tablet") {
return "tablet"
- } else if strings.Contains(hostname, "macbook") || strings.Contains(hostname, "laptop") {
+ } else if strings.Contains(hostname, "macbook") || strings.Contains(hostname, "laptop") ||
+ strings.Contains(hostname, "thinkpad") {
return "laptop"
+ } else if strings.Contains(hostname, "imac") || strings.Contains(hostname, "desktop") ||
+ strings.Contains(hostname, "-pc") {
+ return "desktop"
}
- // Guess by MAC prefix (OUI)
+ // Guess by MAC prefix (OUI) — VM hypervisor MACs are almost always laptops
if len(mac) >= 8 {
macPrefix := strings.ToUpper(mac[:8])
switch macPrefix {
@@ -107,8 +114,8 @@ func GuessDeviceType(hostname, mac string) string {
return "laptop"
case "08:00:27": // VirtualBox
return "laptop"
- default:
- return "mobile"
+ case "52:54:00": // QEMU/KVM
+ return "laptop"
}
}
diff --git a/internal/station/hostapd/backend.go b/internal/station/hostapd/backend.go
index c78c0be..0aba910 100644
--- a/internal/station/hostapd/backend.go
+++ b/internal/station/hostapd/backend.go
@@ -27,17 +27,19 @@ type Backend struct {
stopCh chan struct{}
stopOnce sync.Once
- // IP correlation - will be populated by periodic DHCP lease correlation
- ipByMAC map[string]string // MAC -> IP mapping
+ // IP / hostname correlation - populated by periodic DHCP lease correlation
+ ipByMAC map[string]string // MAC -> IP mapping
+ hostnameByMAC map[string]string // MAC -> hostname 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{}),
+ stations: make(map[string]*HostapdStation),
+ ipByMAC: make(map[string]string),
+ hostnameByMAC: make(map[string]string),
+ hostapdCLI: "hostapd_cli",
+ stopCh: make(chan struct{}),
}
}
@@ -298,12 +300,8 @@ func (b *Backend) parseAllStaOutput(output string) map[string]*HostapdStation {
// 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
+ hostname := b.hostnameByMAC[mac]
return backend.Station{
MAC: mac,
@@ -317,27 +315,34 @@ func (b *Backend) convertStation(mac string, hs *HostapdStation) backend.Station
}
}
-// 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) {
+// UpdateLeaseInfo updates MAC -> IP and MAC -> hostname mappings from an
+// external source (e.g., DHCP lease file). Called periodically by the
+// correlator. An empty hostname leaves the existing one in place — DHCP
+// leases sometimes drop the hostname between renewals.
+func (b *Backend) UpdateLeaseInfo(macToIP, macToHostname 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
+ if oldIP, exists := b.ipByMAC[mac]; !exists || oldIP != ip {
updated[mac] = true
}
b.ipByMAC[mac] = ip
}
- // Trigger update callbacks for stations that got new/changed IPs
+ for mac, hostname := range macToHostname {
+ if hostname == "" {
+ continue
+ }
+ if oldName, exists := b.hostnameByMAC[mac]; !exists || oldName != hostname {
+ updated[mac] = true
+ }
+ b.hostnameByMAC[mac] = hostname
+ }
+
+ // Trigger update callbacks for stations whose info changed
for mac := range updated {
if station, exists := b.stations[mac]; exists {
if cb := b.callbacks.OnStationUpdated; cb != nil {
diff --git a/internal/station/hostapd/correlation.go b/internal/station/hostapd/correlation.go
index fe4361a..c653dce 100644
--- a/internal/station/hostapd/correlation.go
+++ b/internal/station/hostapd/correlation.go
@@ -81,14 +81,20 @@ func (dc *DHCPCorrelator) correlate() {
return
}
- // Build MAC -> IP mapping
+ // Build MAC -> IP and MAC -> hostname mappings. Hostnames live in the
+ // dhcpd "client-hostname" field and let us label devices instead of
+ // falling back to bare MAC addresses.
macToIP := make(map[string]string)
+ macToHostname := make(map[string]string)
for _, lease := range leases {
- macToIP[lease.MAC] = lease.IP
+ mac := strings.ToLower(lease.MAC)
+ macToIP[mac] = lease.IP
+ if lease.Hostname != "" {
+ macToHostname[mac] = lease.Hostname
+ }
}
- // Update backend with IP mappings
- dc.backend.UpdateIPMapping(macToIP)
+ dc.backend.UpdateLeaseInfo(macToIP, macToHostname)
}
// parseDHCPLeases reads and parses DHCP lease file