From a758c331c0ebf8159807adef878c9b47965915a1 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Fri, 1 May 2026 22:21:14 +0800 Subject: [PATCH] station: Default device type to "unknown" and propagate DHCP hostname GuessDeviceType silently returned "mobile" for any device whose hostname or MAC OUI didn't match a known pattern, so the UI labelled every unidentified device as a phone. Default to "unknown" instead and broaden hostname matching (pixel/galaxy, thinkpad, imac/-pc, QEMU OUI). The hostapd backend was also dropping DHCP hostnames on the floor: the correlator only forwarded MAC->IP, and convertStation hard-coded the hostname to "". Replace UpdateIPMapping with UpdateLeaseInfo that carries both maps so hostnames flow through to ConnectedDevice.Name. Frontend gains a "Sans nom" fallback when no hostname is available and French labels for the device-type badge. --- cmd/repeater/static/app.js | 23 +++++++++--- internal/station/backend/types.go | 19 ++++++---- internal/station/hostapd/backend.go | 47 ++++++++++++++----------- internal/station/hostapd/correlation.go | 14 +++++--- 4 files changed, 68 insertions(+), 35 deletions(-) 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