diff --git a/packages/server-admin-ui/src/views/Dashboard/Dashboard.js b/packages/server-admin-ui/src/views/Dashboard/Dashboard.js
index 403322a22..929ae95ee 100644
--- a/packages/server-admin-ui/src/views/Dashboard/Dashboard.js
+++ b/packages/server-admin-ui/src/views/Dashboard/Dashboard.js
@@ -84,7 +84,7 @@ const Dashboard = (props) => {
         
           {linkType === 'plugin'
             ? pluginNameLink(providerId)
-            : providerIdLink(providerId)}
+            : providerIdLink(providerId, providerStats.displayName)}
         
         {providerStats.writeRate > 0 && (
           
@@ -285,13 +285,14 @@ function pluginNameLink(id) {
   return {id}
 }
 
-function providerIdLink(id) {
+function providerIdLink(id, displayName) {
+  const linkText = displayName || id
   if (id === 'defaults') {
-    return {id}
+    return {linkText}
   } else if (id.startsWith('ws.')) {
-    return {id}
+    return {linkText}
   } else {
-    return {id}
+    return {linkText}
   }
 }
 
diff --git a/src/deltastats.ts b/src/deltastats.ts
index d59ede8da..d9381d503 100644
--- a/src/deltastats.ts
+++ b/src/deltastats.ts
@@ -17,6 +17,11 @@
 
 import { isUndefined, values } from 'lodash'
 import { EventEmitter } from 'node:events'
+import { resolveDeviceName } from './deviceNameResolver'
+import { deviceRegistryCache } from './deviceRegistryCache'
+import { createDebug } from './debug'
+
+const debug = createDebug('signalk-server:deltastats')
 
 const STATS_UPDATE_INTERVAL_SECONDS = 5
 export const CONNECTION_WRITE_EVENT_NAME = 'connectionwrite'
@@ -33,6 +38,7 @@ class ProviderStats {
   deltaRate: number
   deltaCount: number
   lastIntervalDeltaCount: number
+  displayName?: string
   constructor() {
     this.writeRate =
       this.writeCount =
@@ -73,6 +79,89 @@ export function startDeltaStatistics(
   return setInterval(() => {
     updateProviderPeriodStats(app)
     const anyApp = app as any
+
+    // Add display names for WebSocket connections
+    if (anyApp.interfaces?.ws) {
+      // Use device registry cache for background stats generation
+      const devices = deviceRegistryCache.getAllDevices()
+      const activeClients = anyApp.interfaces.ws.getActiveClients()
+      debug('Active WebSocket clients:', activeClients.length)
+      debug(
+        'Cached devices:',
+        devices.map((d) => ({
+          clientId: d.clientId,
+          description: d.description
+        }))
+      )
+
+      Object.keys(app.providerStatistics).forEach((providerId) => {
+        if (providerId.startsWith('ws.')) {
+          debug('Processing WebSocket provider:', providerId)
+
+          // Find matching client by various ID formats
+          const clientInfo = activeClients.find((c: any) => {
+            // Try direct ID match first
+            if (`ws.${c.id}` === providerId) {
+              debug('Found matching client by direct ID:', c.id)
+              return true
+            }
+
+            // Try principal identifier match
+            if (
+              c.skPrincipal?.identifier &&
+              `ws.${c.skPrincipal.identifier.replace(/\./g, '_')}` ===
+                providerId
+            ) {
+              debug(
+                'Found matching client by principal:',
+                c.skPrincipal.identifier
+              )
+              return true
+            }
+
+            return false
+          })
+
+          if (clientInfo) {
+            debug('Client info:', {
+              id: clientInfo.id,
+              principal: clientInfo.skPrincipal?.identifier,
+              userAgent: clientInfo.userAgent
+            })
+
+            // Use device registry cache for name resolution
+            // Try multiple ID formats for device lookup
+            let deviceId = clientInfo.id
+
+            // If we have a principal identifier, it might be the device ID
+            if (clientInfo.skPrincipal?.identifier) {
+              // Check if any device matches the principal identifier
+              const deviceByPrincipal = devices.find(
+                (d) => d.clientId === clientInfo.skPrincipal.identifier
+              )
+              if (deviceByPrincipal) {
+                deviceId = clientInfo.skPrincipal.identifier
+                debug('Found device by principal identifier:', deviceId)
+              }
+            }
+
+            const displayName = resolveDeviceName(deviceId, devices, clientInfo)
+            app.providerStatistics[providerId].displayName = displayName
+            debug(
+              'Resolved display name:',
+              displayName,
+              'for',
+              providerId,
+              'using device ID:',
+              deviceId
+            )
+          } else {
+            debug('No matching client found for', providerId)
+          }
+        }
+      })
+    }
+
     app.emit('serverevent', {
       type: 'SERVERSTATISTICS',
       from: 'signalk-server',
diff --git a/src/deviceNameResolver.ts b/src/deviceNameResolver.ts
new file mode 100644
index 000000000..7f6f05844
--- /dev/null
+++ b/src/deviceNameResolver.ts
@@ -0,0 +1,96 @@
+/*
+ * Device Name Resolution Utility
+ * Resolves WebSocket client IDs to user-friendly display names
+ */
+
+interface Device {
+  clientId: string
+  description?: string
+}
+
+interface ClientInfo {
+  skPrincipal?: { name?: string }
+  userAgent?: string
+}
+
+/**
+ * Resolves a WebSocket client ID to a user-friendly display name using a 4-level priority system.
+ *
+ * This function attempts to find the most descriptive name for a connected client by checking
+ * multiple sources in order of preference. The goal is to provide meaningful device names
+ * in the Dashboard instead of cryptic WebSocket IDs like "ws.85d5c860-d34f-42ba-b9f1-b4ba78de8e95".
+ *
+ * Resolution Priority (first match wins):
+ * 1. **Device Description from Registry** - If the device is registered in the security system,
+ *    use its configured description (e.g., "SensESP device: esp32-wireless")
+ * 2. **Principal Name from Authentication** - If the client is authenticated, use the principal's
+ *    name from the authentication context
+ * 3. **Parsed User Agent** - Extract a meaningful name from the User-Agent header:
+ *    - "SensESP" → "SensESP Device"
+ *    - "SignalK" → "SignalK Client"
+ *    - "OpenCPN" → "OpenCPN"
+ *    - Browser agents → "Web Browser"
+ *    - Other agents → First meaningful part of the UA string
+ * 4. **Client ID Fallback** - If no other information is available, return the original client ID
+ *
+ * @param clientId - The WebSocket client ID to resolve (e.g., "ws.123e4567-e89b-12d3-a456-426614174000")
+ * @param devices - Array of registered devices from the device registry cache
+ * @param clientInfo - Optional client information including authentication principal and user agent
+ * @returns A user-friendly display name for the client
+ *
+ * @example
+ * // With a registered device
+ * resolveDeviceName('esp32-001', devices, clientInfo)
+ * // Returns: "SensESP device: esp32-wireless"
+ *
+ * @example
+ * // With only user agent
+ * resolveDeviceName('ws.abc123', [], { userAgent: 'OpenCPN/5.6.2' })
+ * // Returns: "OpenCPN"
+ *
+ * @example
+ * // Fallback case
+ * resolveDeviceName('ws.xyz789', [], {})
+ * // Returns: "ws.xyz789"
+ */
+export function resolveDeviceName(
+  clientId: string,
+  devices: Device[],
+  clientInfo?: ClientInfo
+): string {
+  // 1. Device description from registry
+  const device = devices.find((d) => d.clientId === clientId)
+  if (device?.description) {
+    return device.description
+  }
+
+  // 2. Principal name from authentication
+  if (clientInfo?.skPrincipal?.name) {
+    return clientInfo.skPrincipal.name
+  }
+
+  // 3. User agent (shortened)
+  if (clientInfo?.userAgent) {
+    const ua = clientInfo.userAgent
+    if (ua.includes('SensESP')) {
+      return 'SensESP Device'
+    } else if (ua.includes('SignalK')) {
+      return 'SignalK Client'
+    } else if (ua.includes('OpenCPN')) {
+      return 'OpenCPN'
+    } else if (
+      ua.includes('Chrome') ||
+      ua.includes('Firefox') ||
+      ua.includes('Safari')
+    ) {
+      return 'Web Browser'
+    } else {
+      // Take first meaningful part of user agent
+      const parts = ua.split(/[\s\/\(]/)
+      return parts[0] || 'Unknown Client'
+    }
+  }
+
+  // 4. Fall back to client ID
+  return clientId
+}
diff --git a/src/deviceRegistryCache.ts b/src/deviceRegistryCache.ts
new file mode 100644
index 000000000..1be9f081b
--- /dev/null
+++ b/src/deviceRegistryCache.ts
@@ -0,0 +1,112 @@
+/*
+ * Device Registry Cache Manager
+ * Maintains an in-memory cache of device registry data with event-driven updates
+ */
+
+import { EventEmitter } from 'events'
+import { createDebug } from './debug'
+
+const debug = createDebug('signalk-server:device-registry-cache')
+
+export interface Device {
+  clientId: string
+  permissions: string
+  description?: string
+  config?: Record
+}
+
+interface CacheEventEmitter extends EventEmitter {
+  on(event: 'updated', listener: () => void): this
+  emit(event: 'updated'): boolean
+}
+
+export class DeviceRegistryCache {
+  private devices: Map = new Map()
+  private events: CacheEventEmitter = new EventEmitter()
+
+  constructor() {
+    debug('Device registry cache initialized')
+  }
+
+  /**
+   * Initialize cache with device data
+   */
+  initialize(devices: Device[]): void {
+    this.devices.clear()
+    devices.forEach((device) => {
+      this.devices.set(device.clientId, device)
+    })
+    debug(`Cache initialized with ${this.devices.size} devices`)
+    this.events.emit('updated')
+  }
+
+  /**
+   * Update cache with new device data
+   */
+  update(devices: Device[]): void {
+    const previousSize = this.devices.size
+    this.initialize(devices)
+    if (previousSize !== this.devices.size) {
+      debug(`Cache updated: ${previousSize} -> ${this.devices.size} devices`)
+    }
+  }
+
+  /**
+   * Get device by client ID
+   */
+  getDevice(clientId: string): Device | undefined {
+    return this.devices.get(clientId)
+  }
+
+  /**
+   * Get all devices
+   */
+  getAllDevices(): Device[] {
+    return Array.from(this.devices.values())
+  }
+
+  /**
+   * Add or update a single device
+   */
+  setDevice(device: Device): void {
+    const isNew = !this.devices.has(device.clientId)
+    this.devices.set(device.clientId, device)
+    debug(`Device ${isNew ? 'added' : 'updated'}: ${device.clientId}`)
+    this.events.emit('updated')
+  }
+
+  /**
+   * Remove a device
+   */
+  removeDevice(clientId: string): boolean {
+    const removed = this.devices.delete(clientId)
+    if (removed) {
+      debug(`Device removed: ${clientId}`)
+      this.events.emit('updated')
+    }
+    return removed
+  }
+
+  /**
+   * Subscribe to cache updates
+   */
+  onUpdate(listener: () => void): () => void {
+    this.events.on('updated', listener)
+    return () => {
+      this.events.removeListener('updated', listener)
+    }
+  }
+
+  /**
+   * Get cache statistics
+   */
+  getStats(): { deviceCount: number; cacheSize: number } {
+    return {
+      deviceCount: this.devices.size,
+      cacheSize: JSON.stringify(Array.from(this.devices.values())).length
+    }
+  }
+}
+
+// Singleton instance
+export const deviceRegistryCache = new DeviceRegistryCache()
diff --git a/src/deviceRegistryCacheInit.ts b/src/deviceRegistryCacheInit.ts
new file mode 100644
index 000000000..2bca6681e
--- /dev/null
+++ b/src/deviceRegistryCacheInit.ts
@@ -0,0 +1,96 @@
+/*
+ * Device Registry Cache Initialization
+ * Initializes and maintains the device registry cache with security config updates
+ */
+
+import { deviceRegistryCache, Device } from './deviceRegistryCache'
+import { WithSecurityStrategy } from './security'
+import { createDebug } from './debug'
+import { EventEmitter } from 'events'
+
+const debug = createDebug('signalk-server:device-registry-cache-init')
+
+interface AppWithEvents extends WithSecurityStrategy, EventEmitter {
+  securityStrategy: WithSecurityStrategy['securityStrategy'] & {
+    getDevices?: (config: Record) => Device[]
+    getConfiguration?: () => Record
+  }
+}
+
+/**
+ * Initializes the device registry cache and sets up event listeners for real-time updates.
+ *
+ * This function performs the following tasks:
+ * 1. Loads all existing devices from the security configuration into the cache
+ * 2. Sets up event listeners for security configuration changes to reload all devices
+ * 3. Sets up event listeners for individual device operations (add/update/remove) for real-time updates
+ *
+ * The cache is used by the delta statistics module to resolve WebSocket client IDs to user-friendly
+ * device names that are displayed in the Dashboard. This provides a better user experience by showing
+ * descriptive names instead of cryptic WebSocket IDs.
+ *
+ * @param app - The SignalK server application instance with security strategy and event emitter capabilities
+ *
+ * @example
+ * // Called during server initialization after security is set up
+ * initializeDeviceRegistryCache(app)
+ *
+ * @remarks
+ * - Requires the security strategy to be initialized before calling
+ * - Gracefully handles cases where security is not enabled or devices are not supported
+ * - All errors are caught and logged to prevent server startup failures
+ */
+export function initializeDeviceRegistryCache(app: AppWithEvents) {
+  debug('Initializing device registry cache')
+
+  // Initial load of devices
+  loadDevices(app)
+
+  // Listen for security configuration changes
+  if (app.on) {
+    app.on('securityConfigChange', () => {
+      debug('Security config changed, updating device registry cache')
+      loadDevices(app)
+    })
+  }
+
+  // Also listen for specific device updates if available
+  if (app.on) {
+    app.on('deviceAdded', (device: Device) => {
+      debug('Device added:', device.clientId)
+      deviceRegistryCache.setDevice(device)
+    })
+
+    app.on('deviceUpdated', (device: Device) => {
+      debug('Device updated:', device.clientId)
+      deviceRegistryCache.setDevice(device)
+    })
+
+    app.on('deviceRemoved', (clientId: string) => {
+      debug('Device removed:', clientId)
+      deviceRegistryCache.removeDevice(clientId)
+    })
+  }
+}
+
+function loadDevices(app: AppWithEvents) {
+  try {
+    if (
+      app.securityStrategy &&
+      typeof app.securityStrategy.getDevices === 'function'
+    ) {
+      // Get the current configuration
+      const config = app.securityStrategy.getConfiguration
+        ? app.securityStrategy.getConfiguration()
+        : {}
+
+      const devices = app.securityStrategy.getDevices(config)
+      debug(`Loading ${devices.length} devices into cache`)
+      deviceRegistryCache.initialize(devices)
+    } else {
+      debug('Security strategy does not support getDevices')
+    }
+  } catch (error) {
+    console.error('Error loading devices into cache:', error)
+  }
+}
diff --git a/src/index.ts b/src/index.ts
index d14286e5f..3d6e5664c 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -62,6 +62,7 @@ import { setupCors } from './cors'
 import SubscriptionManager from './subscriptionmanager'
 import { PluginId, PluginManager } from './interfaces/plugins'
 import { OpenApiDescription, OpenApiRecord } from './api/swagger'
+import { initializeDeviceRegistryCache } from './deviceRegistryCacheInit'
 import { WithProviderStatistics } from './deltastats'
 import { pipedProviders } from './pipedproviders'
 import { EventsActorId, WithWrappedEmitter, wrapEmitter } from './events'
@@ -101,6 +102,9 @@ class Server {
     setupCors(app, getSecurityConfig(app))
     startSecurity(app, opts ? opts.securityConfig : null)
 
+    // Initialize device registry cache after security is set up
+    initializeDeviceRegistryCache(app)
+
     require('./serverroutes')(app, saveSecurityConfig, getSecurityConfig)
     require('./put').start(app)
 
diff --git a/src/interfaces/ws.js b/src/interfaces/ws.js
index 4a6f095c4..da8c90c7f 100644
--- a/src/interfaces/ws.js
+++ b/src/interfaces/ws.js
@@ -54,6 +54,34 @@ module.exports = function (app) {
     return count
   }
 
+  api.getActiveClients = function (securityContext) {
+    const clients = []
+    primuses.forEach((primus) =>
+      primus.forEach((spark) => {
+        const clientInfo = {
+          id: spark.id,
+          skPrincipal: spark.request.skPrincipal,
+          remoteAddress:
+            spark.request.headers['x-forwarded-for'] ||
+            spark.request.connection.remoteAddress,
+          userAgent: spark.request.headers['user-agent'],
+          connectedAt: spark.request.connectedAt || new Date().toISOString()
+        }
+
+        // If security context is provided, enhance with device description
+        if (securityContext && securityContext.getDevice) {
+          const device = securityContext.getDevice(spark.id)
+          if (device && device.description) {
+            clientInfo.deviceDescription = device.description
+          }
+        }
+
+        clients.push(clientInfo)
+      })
+    )
+    return clients
+  }
+
   api.canHandlePut = function (path, source) {
     const sources = pathSources[path]
     return sources && (!source || sources[source])
diff --git a/src/serverroutes.ts b/src/serverroutes.ts
index 3cd58da08..fe830d8cc 100644
--- a/src/serverroutes.ts
+++ b/src/serverroutes.ts
@@ -324,11 +324,25 @@ module.exports = function (
           config,
           req.params.uuid,
           req.body,
-          getConfigSavingCallback(
-            'Device updated',
-            'Unable to update device',
-            res
-          )
+          (err: unknown, updatedConfig: unknown) => {
+            if (!err && updatedConfig) {
+              // Find the updated device
+              const config = updatedConfig as {
+                devices?: Array<{ clientId: string }>
+              }
+              const updatedDevice = config.devices?.find(
+                (d) => d.clientId === req.params.uuid
+              )
+              if (updatedDevice) {
+                app.emit('deviceUpdated', updatedDevice)
+              }
+            }
+            getConfigSavingCallback(
+              'Device updated',
+              'Unable to update device',
+              res
+            )(err, updatedConfig)
+          }
         )
       }
     }
@@ -339,14 +353,20 @@ module.exports = function (
     (req: Request, res: Response) => {
       if (checkAllowConfigure(req, res)) {
         const config = getSecurityConfig(app)
+        const deviceId = req.params.uuid
         app.securityStrategy.deleteDevice(
           config,
-          req.params.uuid,
-          getConfigSavingCallback(
-            'Device deleted',
-            'Unable to delete device',
-            res
-          )
+          deviceId,
+          (err: unknown, updatedConfig: unknown) => {
+            if (!err && updatedConfig) {
+              app.emit('deviceRemoved', deviceId)
+            }
+            getConfigSavingCallback(
+              'Device deleted',
+              'Unable to delete device',
+              res
+            )(err, updatedConfig)
+          }
         )
       }
     }
diff --git a/test/deviceNameResolver.js b/test/deviceNameResolver.js
new file mode 100644
index 000000000..3202a6622
--- /dev/null
+++ b/test/deviceNameResolver.js
@@ -0,0 +1,105 @@
+const chai = require('chai')
+chai.Should()
+const { resolveDeviceName } = require('../src/deviceNameResolver')
+
+describe('Device Name Resolution', () => {
+  it('returns device description as first priority', () => {
+    const devices = [
+      { clientId: 'test-client', description: 'Test Device Description' }
+    ]
+    const clientInfo = {
+      skPrincipal: { name: 'Test User' },
+      userAgent: 'Mozilla/5.0'
+    }
+
+    const result = resolveDeviceName('test-client', devices, clientInfo)
+    result.should.equal('Test Device Description')
+  })
+
+  it('returns principal name as second priority', () => {
+    const devices = []
+    const clientInfo = {
+      skPrincipal: { name: 'Test User' },
+      userAgent: 'Mozilla/5.0'
+    }
+
+    const result = resolveDeviceName('test-client', devices, clientInfo)
+    result.should.equal('Test User')
+  })
+
+  it('returns parsed user agent as third priority', () => {
+    const devices = []
+    const clientInfo = {
+      userAgent: 'SensESP/2.0'
+    }
+
+    const result = resolveDeviceName('test-client', devices, clientInfo)
+    result.should.equal('SensESP Device')
+  })
+
+  it('correctly identifies OpenCPN', () => {
+    const devices = []
+    const clientInfo = {
+      userAgent: 'OpenCPN/5.6.2'
+    }
+
+    const result = resolveDeviceName('test-client', devices, clientInfo)
+    result.should.equal('OpenCPN')
+  })
+
+  it('correctly identifies SignalK clients', () => {
+    const devices = []
+    const clientInfo = {
+      userAgent: 'SignalK/1.0'
+    }
+
+    const result = resolveDeviceName('test-client', devices, clientInfo)
+    result.should.equal('SignalK Client')
+  })
+
+  it('correctly identifies web browsers', () => {
+    const devices = []
+    const clientInfo = {
+      userAgent:
+        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
+    }
+
+    const result = resolveDeviceName('test-client', devices, clientInfo)
+    result.should.equal('Web Browser')
+  })
+
+  it('extracts first part of unknown user agents', () => {
+    const devices = []
+    const clientInfo = {
+      userAgent: 'ESP32-Device/1.0 (Custom Firmware)'
+    }
+
+    const result = resolveDeviceName('test-client', devices, clientInfo)
+    result.should.equal('ESP32-Device')
+  })
+
+  it('returns client ID as fallback', () => {
+    const devices = []
+    const clientInfo = {}
+
+    const result = resolveDeviceName('test-client', devices, clientInfo)
+    result.should.equal('test-client')
+  })
+
+  it('handles missing clientInfo gracefully', () => {
+    const devices = []
+
+    const result = resolveDeviceName('test-client', devices)
+    result.should.equal('test-client')
+  })
+
+  it('handles empty user agent gracefully', () => {
+    const devices = []
+    const clientInfo = {
+      userAgent: ''
+    }
+
+    const result = resolveDeviceName('test-client', devices, clientInfo)
+    result.should.equal('test-client')
+  })
+})