Browse Source

Add MAC addresses display, Kernel Log tab, and change device_id to eth0

Dashboard improvements:
- Show eth0 MAC instead of wlan0 MAC in header
- Add MAC addresses for both Ethernet and Wireless interfaces in Status tab
- Add new Kernel Log tab with dmesg output

Backend changes:
- Add eth0_mac and wlan0_mac fields to NetworkStatus API
- Add getMacAddress() helper function
- Add /api/kernel-logs endpoint for dmesg output
- Handle BusyBox dmesg without -T flag support
- Change device_id priority from wlan0 to eth0 for consistent identification

Frontend changes:
- Update App.vue to display eth0 MAC in header
- Update StatusTab.vue to show MAC after TX Total for each interface
- Add KernelLogTab.vue component with filtering and syntax highlighting
- Add Kernel Log tab to navigation

Device ID change:
- eth0 MAC: 38:54:39:4b:1b:ad (new device_id)
- wlan0 MAC: 38:54:39:4b:1b:ac (old device_id)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
root 1 month ago
parent
commit
931c80b6f7

+ 47 - 0
cmd/beacon-daemon/api.go

@@ -56,9 +56,11 @@ type StatusResponse struct {
 
 type NetworkStatus struct {
 	Eth0IP       string `json:"eth0_ip,omitempty"`
+	Eth0MAC      string `json:"eth0_mac,omitempty"`
 	Eth0RX       int64  `json:"eth0_rx,omitempty"`
 	Eth0TX       int64  `json:"eth0_tx,omitempty"`
 	Wlan0IP      string `json:"wlan0_ip,omitempty"`
+	Wlan0MAC     string `json:"wlan0_mac,omitempty"`
 	Wlan0SSID    string `json:"wlan0_ssid,omitempty"`
 	Wlan0Signal  int    `json:"wlan0_signal,omitempty"`
 	Wlan0Channel int    `json:"wlan0_channel,omitempty"`
@@ -164,6 +166,7 @@ func (s *APIServer) Start(addr string) error {
 	mux.HandleFunc("/api/settings", s.handleSettings)
 	mux.HandleFunc("/api/unlock", s.handleUnlock)
 	mux.HandleFunc("/api/logs", s.handleLogs)
+	mux.HandleFunc("/api/kernel-logs", s.handleKernelLogs)
 	mux.HandleFunc("/api/ws", s.handleWebSocket)
 
 	// Serve static files for dashboard with SPA fallback
@@ -261,9 +264,11 @@ func (s *APIServer) handleStatus(w http.ResponseWriter, r *http.Request) {
 		Uptime:     getUptime(),
 		Network: NetworkStatus{
 			Eth0IP:       getInterfaceIP("eth0"),
+			Eth0MAC:      getMacAddress("eth0"),
 			Eth0RX:       eth0rx,
 			Eth0TX:       eth0tx,
 			Wlan0IP:      getInterfaceIP("wlan0"),
+			Wlan0MAC:     getMacAddress("wlan0"),
 			Wlan0SSID:    wlanInfo.ssid,
 			Wlan0Signal:  wlanInfo.signal,
 			Wlan0Channel: wlanInfo.channel,
@@ -344,6 +349,38 @@ func (s *APIServer) handleLogs(w http.ResponseWriter, r *http.Request) {
 	s.jsonResponse(w, result)
 }
 
+// handleKernelLogs returns recent kernel log lines from dmesg
+func (s *APIServer) handleKernelLogs(w http.ResponseWriter, r *http.Request) {
+	// Run dmesg command
+	cmd := exec.Command("dmesg", "-T")
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		// Try without -T flag (human-readable timestamps) if not supported
+		cmd = exec.Command("dmesg")
+		output, err = cmd.CombinedOutput()
+		if err != nil {
+			s.jsonResponse(w, []string{})
+			return
+		}
+	}
+
+	lines := strings.Split(string(output), "\n")
+	// Return last 500 lines
+	if len(lines) > 500 {
+		lines = lines[len(lines)-500:]
+	}
+
+	// Filter out empty lines
+	var result []string
+	for _, line := range lines {
+		if strings.TrimSpace(line) != "" {
+			result = append(result, line)
+		}
+	}
+
+	s.jsonResponse(w, result)
+}
+
 // handleConfig returns current config (without secrets)
 func (s *APIServer) handleConfig(w http.ResponseWriter, r *http.Request) {
 	s.daemon.mu.Lock()
@@ -789,6 +826,16 @@ func freqToChannel(freq int) int {
 	return 0
 }
 
+// getMacAddress returns MAC address for given interface
+func getMacAddress(iface string) string {
+	addressPath := "/sys/class/net/" + iface + "/address"
+	data, err := os.ReadFile(addressPath)
+	if err != nil {
+		return ""
+	}
+	return strings.TrimSpace(string(data))
+}
+
 // applyWiFiSettings configures wpa_supplicant and connects to WiFi
 func applyWiFiSettings(ssid, psk string) error {
 	// Retry the whole connection up to 3 times

+ 2 - 2
cmd/beacon-daemon/main.go

@@ -730,8 +730,8 @@ func (d *Daemon) updateWiFiCredentials(ssid, psk string) error {
 
 // getDeviceID returns a device ID based on MAC address
 func getDeviceID() string {
-	// Try wlan0 first, then eth0
-	for _, iface := range []string{"wlan0", "eth0"} {
+	// Try eth0 first, then wlan0
+	for _, iface := range []string{"eth0", "wlan0"} {
 		if mac := getInterfaceMAC(iface); mac != "" {
 			return mac
 		}

+ 19 - 1
dashboard/src/App.vue

@@ -4,7 +4,7 @@
       <div class="header-left">
         <span class="status-indicator" :class="daemonOk ? 'ok' : 'error'"></span>
         <h1>MyBeacon</h1>
-        <span class="device-badge">Device {{ status.device_id || '...' }}</span>
+        <span class="device-badge">Device {{ status.network?.eth0_mac || '...' }}</span>
       </div>
     </header>
 
@@ -24,6 +24,7 @@
       <BLETab v-else-if="activeTab === 'ble'" :events="bleEvents" />
       <WiFiTab v-else-if="activeTab === 'wifi'" :events="wifiEvents" :wifiClientActive="!!status.network?.wlan0_ip" />
       <LogTab v-else-if="activeTab === 'log'" :logs="logs" />
+      <KernelLogTab v-else-if="activeTab === 'kernel-log'" :logs="kernelLogs" />
       <SettingsTab v-else-if="activeTab === 'settings'" :config="config" :unlocked="unlocked" :message="settingsMessage" @unlock="handleUnlock" @save="handleSave" />
     </main>
 
@@ -36,6 +37,7 @@ import StatusTab from './components/StatusTab.vue'
 import BLETab from './components/BLETab.vue'
 import WiFiTab from './components/WiFiTab.vue'
 import LogTab from './components/LogTab.vue'
+import KernelLogTab from './components/KernelLogTab.vue'
 import SettingsTab from './components/SettingsTab.vue'
 
 const API_BASE = import.meta.env.VITE_API_BASE || ''
@@ -58,6 +60,7 @@ const tabs = computed(() => {
   }
 
   result.push({ id: 'log', label: 'Daemon Log' })
+  result.push({ id: 'kernel-log', label: 'Kernel Log' })
   result.push({ id: 'settings', label: 'Settings' })
   return result
 })
@@ -69,6 +72,7 @@ const config = ref({})
 const bleEvents = ref([])
 const wifiEvents = ref([])
 const logs = ref([])
+const kernelLogs = ref([])
 const unlocked = ref(false)
 const devicePassword = ref('')
 const settingsMessage = ref({ text: '', type: '' })
@@ -139,6 +143,18 @@ async function fetchLogs() {
   }
 }
 
+async function fetchKernelLogs() {
+  try {
+    const res = await fetch(`${API_BASE}/api/kernel-logs`)
+    const data = await res.json()
+    if (Array.isArray(data)) {
+      kernelLogs.value = data
+    }
+  } catch (e) {
+    console.error('Kernel logs fetch error:', e)
+  }
+}
+
 function connectWebSocket() {
   const wsUrl = `${location.protocol === 'https:' ? 'wss:' : 'ws:'}//${location.host}${API_BASE}/api/ws`
   ws = new WebSocket(wsUrl)
@@ -222,12 +238,14 @@ onMounted(() => {
   fetchBLE()
   fetchWiFi()
   fetchLogs()
+  fetchKernelLogs()
 
   pollInterval = setInterval(() => {
     fetchStatus()
     fetchMetrics()
     fetchConfig()
     fetchLogs()
+    fetchKernelLogs()
   }, 5000)
 
   connectWebSocket()

+ 129 - 0
dashboard/src/components/KernelLogTab.vue

@@ -0,0 +1,129 @@
+<template>
+  <div class="log-tab">
+    <div class="toolbar">
+      <input v-model="filter" type="text" placeholder="Filter kernel logs..." class="filter-input" />
+      <span class="count">{{ filteredLogs.length }} lines</span>
+    </div>
+
+    <div class="log-container" ref="logContainer">
+      <div v-for="(line, i) in filteredLogs" :key="i" class="log-line" :class="getLogLevel(line)">
+        {{ line }}
+      </div>
+      <div v-if="filteredLogs.length === 0" class="no-logs">
+        No kernel logs available. Kernel messages (dmesg) will appear here.
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, onMounted, watch, nextTick } from 'vue'
+
+const props = defineProps({
+  logs: Array
+})
+
+const filter = ref('')
+const logContainer = ref(null)
+
+const filteredLogs = computed(() => {
+  const f = filter.value.toLowerCase()
+  return (props.logs || []).filter(line =>
+    !f || line.toLowerCase().includes(f)
+  )
+})
+
+function scrollToBottom() {
+  if (logContainer.value) {
+    logContainer.value.scrollTop = logContainer.value.scrollHeight
+  }
+}
+
+onMounted(() => {
+  nextTick(scrollToBottom)
+})
+
+// Auto-scroll when new logs arrive
+watch(() => props.logs?.length, () => {
+  nextTick(scrollToBottom)
+})
+
+function getLogLevel(line) {
+  const l = line.toLowerCase()
+  // Kernel log levels
+  if (l.includes('error') || l.includes('fail') || l.includes('panic') || l.includes('oops')) return 'error'
+  if (l.includes('warn') || l.includes('warning')) return 'warn'
+  if (l.includes('debug')) return 'debug'
+  return ''
+}
+</script>
+
+<style scoped>
+.log-tab {
+  display: flex;
+  flex-direction: column;
+  gap: 1rem;
+  height: 100%;
+}
+
+.toolbar {
+  display: flex;
+  gap: 1rem;
+  align-items: center;
+  padding: 0.75rem;
+  background: #161b22;
+  border: 1px solid #30363d;
+  border-radius: 6px;
+}
+
+.filter-input {
+  flex: 1;
+  max-width: 300px;
+  padding: 0.4rem 0.6rem;
+  background: #0d1117;
+  border: 1px solid #30363d;
+  border-radius: 4px;
+  color: #c9d1d9;
+  font-size: 0.8rem;
+}
+
+.filter-input::placeholder {
+  color: #6e7681;
+}
+
+.count {
+  margin-left: auto;
+  color: #8b949e;
+  font-size: 0.8rem;
+}
+
+.log-container {
+  flex: 1;
+  background: #0d1117;
+  border-radius: 6px;
+  border: 1px solid #30363d;
+  padding: 1rem;
+  font-family: monospace;
+  font-size: 0.75rem;
+  overflow-y: auto;
+  max-height: calc(100vh - 250px);
+  text-align: left;
+}
+
+.log-line {
+  padding: 0.2rem 0;
+  white-space: pre-wrap;
+  word-break: break-all;
+  color: #c9d1d9;
+}
+
+.log-line.error { color: #ef4444; }
+.log-line.warn { color: #f59e0b; }
+.log-line.debug { color: #6e7681; }
+
+.no-logs {
+  color: #6e7681;
+  text-align: left;
+  padding: 2rem;
+}
+</style>

+ 7 - 0
dashboard/src/components/StatusTab.vue

@@ -44,6 +44,7 @@
             <div class="iface-row"><span>DNS</span><span>{{ status.network?.dns || 'N/A' }}</span></div>
             <div class="iface-row"><span>RX Total</span><span>{{ formatBytes(status.network?.eth0_rx) }}</span></div>
             <div class="iface-row"><span>TX Total</span><span>{{ formatBytes(status.network?.eth0_tx) }}</span></div>
+            <div class="iface-row"><span>MAC</span><span class="mac-addr">{{ status.network?.eth0_mac || 'N/A' }}</span></div>
           </div>
           <div class="iface-card">
             <div class="iface-name">Wireless</div>
@@ -55,6 +56,7 @@
             <div class="iface-row"><span>RSSI</span><span>{{ wlanConnected && status.network?.wlan0_signal ? status.network.wlan0_signal + ' dBm' : 'N/A' }}</span></div>
             <div class="iface-row"><span>RX Total</span><span>{{ wlanConnected ? formatBytes(status.network?.wlan0_rx) : 'N/A' }}</span></div>
             <div class="iface-row"><span>TX Total</span><span>{{ wlanConnected ? formatBytes(status.network?.wlan0_tx) : 'N/A' }}</span></div>
+            <div class="iface-row"><span>MAC</span><span class="mac-addr">{{ status.network?.wlan0_mac || 'N/A' }}</span></div>
           </div>
         </div>
       </div>
@@ -342,6 +344,11 @@ function getRssiClass(rssi) {
   color: #aaa;
 }
 
+.mac-addr {
+  font-family: monospace;
+  color: #58a6ff;
+}
+
 /* Devices Grid */
 .devices-grid {
   display: grid;