feat: expand dashboard WebSocket feed with events, alerts, BLE, triggers, and system health
- Add BroadcastEvent for presence transitions, zone entries/exits, portal crossings - Add BroadcastAlert for anomaly detections and security mode triggers - Add BroadcastBLEScan for BLE device list updates (5s interval) - Add BroadcastTriggerState for automation trigger state changes - Add BroadcastSystemHealth for periodic system stats (60s interval) - Add BLEState, TriggerState, and SystemHealthProvider interfaces - Update hub.Run with new tickers for BLE (5s) and system health (60s) - Update dashboard app.js with handlers for new message types - Add bleDevices map to state for BLE device tracking - Log unhandled message types for future debugging Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
37f013c58d
commit
bfa3e6f2b2
2 changed files with 322 additions and 2 deletions
|
|
@ -52,7 +52,9 @@
|
|||
// Diurnal learning tracking
|
||||
diurnalStatus: new Map(), // linkID -> { is_learning, progress, is_ready, days_remaining }
|
||||
diurnalPollTimer: null,
|
||||
healthPollTimer: null
|
||||
healthPollTimer: null,
|
||||
// BLE device tracking
|
||||
bleDevices: new Map() // MAC -> { mac, name, rssi, last_seen, label, blob_id }
|
||||
};
|
||||
|
||||
// ============================================
|
||||
|
|
@ -417,6 +419,126 @@
|
|||
// ============================================
|
||||
// Message Handling
|
||||
// ============================================
|
||||
|
||||
// Event handlers for new message types
|
||||
|
||||
function handleEventMessage(msg) {
|
||||
if (!msg.event) return;
|
||||
|
||||
const event = msg.event;
|
||||
console.log('[Spaxel] Event:', event.kind, 'in', event.zone, 'by', event.person_name || 'blob #' + event.blob_id);
|
||||
|
||||
// Log to timeline
|
||||
const timeStr = new Date(event.ts).toLocaleTimeString();
|
||||
let description = '';
|
||||
|
||||
if (event.kind === 'zone_entry') {
|
||||
description = (event.person_name || 'Someone') + ' entered ' + event.zone;
|
||||
} else if (event.kind === 'zone_exit') {
|
||||
description = (event.person_name || 'Someone') + ' left ' + event.zone;
|
||||
} else if (event.kind === 'portal_crossing') {
|
||||
description = (event.person_name || 'Someone') + ' crossed portal in ' + event.zone;
|
||||
} else if (event.kind === 'presence_transition') {
|
||||
description = (event.person_name || 'Someone') + ' presence detected in ' + event.zone;
|
||||
} else {
|
||||
description = event.kind + ' in ' + event.zone;
|
||||
}
|
||||
|
||||
logTimelineEvent(event.kind, null, description + ' (' + timeStr + ')');
|
||||
|
||||
// Show toast for security-relevant events
|
||||
if (event.kind === 'zone_entry' || event.kind === 'portal_crossing') {
|
||||
showToast(description, 'info');
|
||||
}
|
||||
}
|
||||
|
||||
function handleAlertMessage(msg) {
|
||||
if (!msg.alert) return;
|
||||
|
||||
const alert = msg.alert;
|
||||
console.log('[Spaxel] Alert:', alert.severity, alert.description);
|
||||
|
||||
// Show toast notification
|
||||
const toastType = alert.severity === 'critical' ? 'error' : 'warning';
|
||||
showToast(alert.description, toastType);
|
||||
|
||||
// Log to timeline
|
||||
const timeStr = new Date(alert.ts).toLocaleTimeString();
|
||||
logTimelineEvent('alert', null, '[' + alert.severity.toUpperCase() + '] ' + alert.description + ' (' + timeStr + ')');
|
||||
|
||||
// Could trigger UI alert state here (e.g., show alert banner)
|
||||
if (window.showAlertBanner) {
|
||||
window.showAlertBanner(alert);
|
||||
}
|
||||
}
|
||||
|
||||
function handleBLEScanMessage(msg) {
|
||||
if (!msg.devices || !Array.isArray(msg.devices)) return;
|
||||
|
||||
console.log('[Spaxel] BLE scan: ' + msg.devices.length + ' devices');
|
||||
|
||||
// Update BLE device list state
|
||||
if (!state.bleDevices) {
|
||||
state.bleDevices = new Map();
|
||||
}
|
||||
|
||||
// Clear previous entries and add current devices
|
||||
state.bleDevices.clear();
|
||||
msg.devices.forEach(function (device) {
|
||||
state.bleDevices.set(device.mac || device.addr, {
|
||||
mac: device.mac || device.addr,
|
||||
name: device.name || device.device_name || 'Unknown',
|
||||
rssi: device.rssi || device.rssi_dbm || 0,
|
||||
last_seen: device.last_seen || Date.now(),
|
||||
label: device.label || '',
|
||||
blob_id: device.blob_id || null
|
||||
});
|
||||
});
|
||||
|
||||
// Update UI if BLE panel exists
|
||||
if (window.BLEPanel && window.BLEPanel.updateDevices) {
|
||||
window.BLEPanel.updateDevices(msg.devices);
|
||||
}
|
||||
}
|
||||
|
||||
function handleTriggerStateMessage(msg) {
|
||||
if (!msg.trigger) return;
|
||||
|
||||
const trigger = msg.trigger;
|
||||
console.log('[Spaxel] Trigger state:', trigger.name, 'enabled=' + trigger.enabled);
|
||||
|
||||
// Update trigger state in UI if automation panel exists
|
||||
if (window.Automations && window.Automations.updateTriggerState) {
|
||||
window.Automations.updateTriggerState(trigger);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSystemHealthMessage(msg) {
|
||||
if (!msg.health) return;
|
||||
|
||||
const health = msg.health;
|
||||
console.log('[Spaxel] System health:', health);
|
||||
|
||||
// Update system health display in UI
|
||||
const healthEl = document.getElementById('system-uptime');
|
||||
if (healthEl) {
|
||||
const uptimeSec = health.uptime_s || 0;
|
||||
const hours = Math.floor(uptimeSec / 3600);
|
||||
const mins = Math.floor((uptimeSec % 3600) / 60);
|
||||
healthEl.textContent = hours + 'h ' + mins + 'm';
|
||||
}
|
||||
|
||||
const memEl = document.getElementById('system-memory');
|
||||
if (memEl) {
|
||||
memEl.textContent = (health.mem_mb || 0).toFixed(1) + ' MB';
|
||||
}
|
||||
|
||||
const goroutinesEl = document.getElementById('system-goroutines');
|
||||
if (goroutinesEl) {
|
||||
goroutinesEl.textContent = health.go_routines || 0;
|
||||
}
|
||||
}
|
||||
|
||||
function handleMessage(data) {
|
||||
if (typeof data === 'string') {
|
||||
// JSON message
|
||||
|
|
@ -580,8 +702,34 @@
|
|||
fetchDiurnalStatus();
|
||||
break;
|
||||
|
||||
case 'event':
|
||||
// Event: presence transition, zone entry/exit, portal crossing
|
||||
handleEventMessage(msg);
|
||||
break;
|
||||
|
||||
case 'alert':
|
||||
// Alert: anomaly detection, security mode trigger
|
||||
handleAlertMessage(msg);
|
||||
break;
|
||||
|
||||
case 'ble_scan':
|
||||
// BLE device list update (5s interval)
|
||||
handleBLEScanMessage(msg);
|
||||
break;
|
||||
|
||||
case 'trigger_state':
|
||||
// Automation trigger state change
|
||||
handleTriggerStateMessage(msg);
|
||||
break;
|
||||
|
||||
case 'system_health':
|
||||
// System health stats (60s interval)
|
||||
handleSystemHealthMessage(msg);
|
||||
break;
|
||||
|
||||
default:
|
||||
// Ignore unknown types (forward-compatible)
|
||||
// Log unhandled types for future debugging
|
||||
console.log('[Spaxel] Unknown message type:', msg.type, msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,11 @@ type Hub struct {
|
|||
|
||||
// Reference to ingestion server for state queries
|
||||
ingestionState IngestionState
|
||||
|
||||
// Additional state providers
|
||||
bleState BLEState
|
||||
triggerState TriggerState
|
||||
systemHealth SystemHealthProvider
|
||||
}
|
||||
|
||||
// IngestionState is an interface to query node/link/motion state from ingestion
|
||||
|
|
@ -32,6 +37,25 @@ type IngestionState interface {
|
|||
GetAllMotionStates() []ingestion.MotionStateItem
|
||||
}
|
||||
|
||||
// BLEState is an interface to query current BLE devices for dashboard broadcast
|
||||
type BLEState interface {
|
||||
GetCurrentDevices() []map[string]interface{}
|
||||
}
|
||||
|
||||
// TriggerState is an interface to query automation trigger states for dashboard broadcast
|
||||
type TriggerState interface {
|
||||
GetTriggerStates() []map[string]interface{}
|
||||
}
|
||||
|
||||
// SystemHealthProvider is an interface to query system health metrics
|
||||
type SystemHealthProvider interface {
|
||||
GetUptimeSeconds() int64
|
||||
GetNodeCount() int
|
||||
GetBeadCount() int
|
||||
GetGoRoutineCount() int
|
||||
GetMemoryMB() float64
|
||||
}
|
||||
|
||||
// Client represents a dashboard WebSocket client
|
||||
type Client struct {
|
||||
hub *Hub
|
||||
|
|
@ -55,6 +79,27 @@ func (h *Hub) SetIngestionState(state IngestionState) {
|
|||
h.mu.Unlock()
|
||||
}
|
||||
|
||||
// SetBLEState sets the BLE state provider
|
||||
func (h *Hub) SetBLEState(state BLEState) {
|
||||
h.mu.Lock()
|
||||
h.bleState = state
|
||||
h.mu.Unlock()
|
||||
}
|
||||
|
||||
// SetTriggerState sets the automation trigger state provider
|
||||
func (h *Hub) SetTriggerState(state TriggerState) {
|
||||
h.mu.Lock()
|
||||
h.triggerState = state
|
||||
h.mu.Unlock()
|
||||
}
|
||||
|
||||
// SetSystemHealth sets the system health provider
|
||||
func (h *Hub) SetSystemHealth(provider SystemHealthProvider) {
|
||||
h.mu.Lock()
|
||||
h.systemHealth = provider
|
||||
h.mu.Unlock()
|
||||
}
|
||||
|
||||
// Run starts the hub's main loop
|
||||
func (h *Hub) Run() {
|
||||
stateTicker := time.NewTicker(5 * time.Second)
|
||||
|
|
@ -63,6 +108,14 @@ func (h *Hub) Run() {
|
|||
presenceTicker := time.NewTicker(500 * time.Millisecond)
|
||||
defer presenceTicker.Stop()
|
||||
|
||||
// BLE scan broadcast ticker (5 seconds)
|
||||
bleScanTicker := time.NewTicker(5 * time.Second)
|
||||
defer bleScanTicker.Stop()
|
||||
|
||||
// System health broadcast ticker (60 seconds)
|
||||
healthTicker := time.NewTicker(60 * time.Second)
|
||||
defer healthTicker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case client := <-h.register:
|
||||
|
|
@ -97,6 +150,12 @@ func (h *Hub) Run() {
|
|||
|
||||
case <-presenceTicker.C:
|
||||
h.broadcastPresence()
|
||||
|
||||
case <-bleScanTicker.C:
|
||||
h.broadcastBLEScan()
|
||||
|
||||
case <-healthTicker.C:
|
||||
h.broadcastSystemHealth()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -390,6 +449,45 @@ func (h *Hub) ClientCount() int {
|
|||
return len(h.clients)
|
||||
}
|
||||
|
||||
// broadcastBLEScan broadcasts the current BLE device list to all dashboard clients.
|
||||
func (h *Hub) broadcastBLEScan() {
|
||||
h.mu.RLock()
|
||||
state := h.bleState
|
||||
clientCount := len(h.clients)
|
||||
h.mu.RUnlock()
|
||||
|
||||
if state == nil || clientCount == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
devices := state.GetCurrentDevices()
|
||||
if len(devices) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
h.BroadcastBLEScan(devices)
|
||||
}
|
||||
|
||||
// broadcastSystemHealth broadcasts system health stats to all dashboard clients.
|
||||
func (h *Hub) broadcastSystemHealth() {
|
||||
h.mu.RLock()
|
||||
provider := h.systemHealth
|
||||
clientCount := len(h.clients)
|
||||
h.mu.RUnlock()
|
||||
|
||||
if provider == nil || clientCount == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
h.BroadcastSystemHealth(
|
||||
provider.GetUptimeSeconds(),
|
||||
provider.GetNodeCount(),
|
||||
provider.GetBeadCount(),
|
||||
provider.GetGoRoutineCount(),
|
||||
provider.GetMemoryMB(),
|
||||
)
|
||||
}
|
||||
|
||||
// BroadcastFleetChange broadcasts a fleet change event to all dashboard clients.
|
||||
// This implements the fleet.FleetChangeBroadcaster interface.
|
||||
func (h *Hub) BroadcastFleetChange(event fleet.FleetChangeEvent) {
|
||||
|
|
@ -522,3 +620,77 @@ func (h *Hub) BroadcastAnomaly(anomaly interface{}) {
|
|||
data, _ := json.Marshal(msg)
|
||||
h.Broadcast(data)
|
||||
}
|
||||
|
||||
// BroadcastEvent broadcasts an event (presence transition, zone entry/exit, portal crossing) to all dashboard clients.
|
||||
func (h *Hub) BroadcastEvent(eventID string, timestamp time.Time, kind, zone string, blobID int, personName string) {
|
||||
msg := map[string]interface{}{
|
||||
"type": "event",
|
||||
"event": map[string]interface{}{
|
||||
"id": eventID,
|
||||
"ts": timestamp.UnixMilli(),
|
||||
"kind": kind,
|
||||
"zone": zone,
|
||||
"blob_id": blobID,
|
||||
"person_name": personName,
|
||||
},
|
||||
}
|
||||
data, _ := json.Marshal(msg)
|
||||
h.Broadcast(data)
|
||||
}
|
||||
|
||||
// BroadcastAlert broadcasts an alert (anomaly detection, security mode trigger) to all dashboard clients.
|
||||
func (h *Hub) BroadcastAlert(alertID string, timestamp time.Time, severity, description string, acknowledged bool) {
|
||||
msg := map[string]interface{}{
|
||||
"type": "alert",
|
||||
"alert": map[string]interface{}{
|
||||
"id": alertID,
|
||||
"ts": timestamp.UnixMilli(),
|
||||
"severity": severity,
|
||||
"description": description,
|
||||
"acknowledged": acknowledged,
|
||||
},
|
||||
}
|
||||
data, _ := json.Marshal(msg)
|
||||
h.Broadcast(data)
|
||||
}
|
||||
|
||||
// BroadcastBLEScan broadcasts BLE device list updates to all dashboard clients (5s interval).
|
||||
func (h *Hub) BroadcastBLEScan(devices []map[string]interface{}) {
|
||||
msg := map[string]interface{}{
|
||||
"type": "ble_scan",
|
||||
"devices": devices,
|
||||
}
|
||||
data, _ := json.Marshal(msg)
|
||||
h.Broadcast(data)
|
||||
}
|
||||
|
||||
// BroadcastTriggerState broadcasts automation trigger state changes to all dashboard clients.
|
||||
func (h *Hub) BroadcastTriggerState(triggerID, name string, lastFired time.Time, enabled bool) {
|
||||
msg := map[string]interface{}{
|
||||
"type": "trigger_state",
|
||||
"trigger": map[string]interface{}{
|
||||
"id": triggerID,
|
||||
"name": name,
|
||||
"last_fired": lastFired.UnixMilli(),
|
||||
"enabled": enabled,
|
||||
},
|
||||
}
|
||||
data, _ := json.Marshal(msg)
|
||||
h.Broadcast(data)
|
||||
}
|
||||
|
||||
// BroadcastSystemHealth broadcasts periodic system health stats to all dashboard clients (60s interval).
|
||||
func (h *Hub) BroadcastSystemHealth(uptimeS int64, nodeCount, beadCount, goRoutines int, memMB float64) {
|
||||
msg := map[string]interface{}{
|
||||
"type": "system_health",
|
||||
"health": map[string]interface{}{
|
||||
"uptime_s": uptimeS,
|
||||
"node_count": nodeCount,
|
||||
"bead_count": beadCount,
|
||||
"go_routines": goRoutines,
|
||||
"mem_mb": memMB,
|
||||
},
|
||||
}
|
||||
data, _ := json.Marshal(msg)
|
||||
h.Broadcast(data)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue