Browse Source

Implement comprehensive monitoring dashboard with real-time charts

Complete rewrite of superadmin dashboard with enterprise-grade monitoring:

Alert System:
- Dismissable alert banners at top of dashboard
- Severity-based coloring (red=critical, yellow=warning)
- Acknowledge and dismiss actions
- Human-readable timestamps (minutes/hours ago)

Device Statistics:
- Total device count
- Online/Offline/Error status breakdown
- Color-coded borders (green=online, gray=offline, red=error)

Host Metrics Visualization (6 comprehensive charts):
- CPU Usage: Line chart with red highlighting when ≥90%
- Memory Usage: Line chart with red highlighting when ≥90%
- Load Average: Multi-line chart (1m, 5m, 15m) with CPU threshold
- Disk I/O: IOPS (read/write operations per second)
- Network Throughput: MB/s inbound/outbound traffic
- Disk Usage: Percentage with red highlighting when ≥90%

Technical Features:
- Chart.js integration with vue-chartjs wrapper
- Real-time data polling (30-second interval)
- Last 30 data points for trend analysis
- Segment-based coloring for threshold violations
- Responsive grid layout adapting to screen size
- Clean card-based UI design

Dependencies added:
- chart.js: Core charting library
- vue-chartjs: Vue 3 wrapper for Chart.js

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
root 4 weeks ago
parent
commit
d8aab22349
3 changed files with 606 additions and 70 deletions
  1. 30 0
      frontend/package-lock.json
  2. 2 0
      frontend/package.json
  3. 574 70
      frontend/src/views/superadmin/DashboardView.vue

+ 30 - 0
frontend/package-lock.json

@@ -9,8 +9,10 @@
       "version": "1.0.0",
       "version": "1.0.0",
       "dependencies": {
       "dependencies": {
         "axios": "^1.6.5",
         "axios": "^1.6.5",
+        "chart.js": "^4.5.1",
         "pinia": "^2.1.7",
         "pinia": "^2.1.7",
         "vue": "^3.4.15",
         "vue": "^3.4.15",
+        "vue-chartjs": "^5.3.3",
         "vue-i18n": "^9.14.5",
         "vue-i18n": "^9.14.5",
         "vue-router": "^4.2.5"
         "vue-router": "^4.2.5"
       },
       },
@@ -506,6 +508,12 @@
       "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
       "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
       "license": "MIT"
       "license": "MIT"
     },
     },
+    "node_modules/@kurkle/color": {
+      "version": "0.3.4",
+      "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
+      "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
+      "license": "MIT"
+    },
     "node_modules/@rollup/rollup-android-arm-eabi": {
     "node_modules/@rollup/rollup-android-arm-eabi": {
       "version": "4.54.0",
       "version": "4.54.0",
       "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz",
       "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz",
@@ -971,6 +979,18 @@
         "node": ">= 0.4"
         "node": ">= 0.4"
       }
       }
     },
     },
+    "node_modules/chart.js": {
+      "version": "4.5.1",
+      "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
+      "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
+      "license": "MIT",
+      "dependencies": {
+        "@kurkle/color": "^0.3.0"
+      },
+      "engines": {
+        "pnpm": ">=8"
+      }
+    },
     "node_modules/combined-stream": {
     "node_modules/combined-stream": {
       "version": "1.0.8",
       "version": "1.0.8",
       "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
       "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -1513,6 +1533,16 @@
         }
         }
       }
       }
     },
     },
+    "node_modules/vue-chartjs": {
+      "version": "5.3.3",
+      "resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.3.tgz",
+      "integrity": "sha512-jqxtL8KZ6YJ5NTv6XzrzLS7osyegOi28UGNZW0h9OkDL7Sh1396ht4Dorh04aKrl2LiSalQ84WtqiG0RIJb0tA==",
+      "license": "MIT",
+      "peerDependencies": {
+        "chart.js": "^4.1.1",
+        "vue": "^3.0.0-0 || ^2.7.0"
+      }
+    },
     "node_modules/vue-demi": {
     "node_modules/vue-demi": {
       "version": "0.14.10",
       "version": "0.14.10",
       "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
       "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",

+ 2 - 0
frontend/package.json

@@ -10,8 +10,10 @@
   },
   },
   "dependencies": {
   "dependencies": {
     "axios": "^1.6.5",
     "axios": "^1.6.5",
+    "chart.js": "^4.5.1",
     "pinia": "^2.1.7",
     "pinia": "^2.1.7",
     "vue": "^3.4.15",
     "vue": "^3.4.15",
+    "vue-chartjs": "^5.3.3",
     "vue-i18n": "^9.14.5",
     "vue-i18n": "^9.14.5",
     "vue-router": "^4.2.5"
     "vue-router": "^4.2.5"
   },
   },

+ 574 - 70
frontend/src/views/superadmin/DashboardView.vue

@@ -1,78 +1,568 @@
 <template>
 <template>
   <div class="dashboard">
   <div class="dashboard">
+    <!-- Alert Banner -->
+    <div v-if="activeAlerts.length > 0" class="alerts-section">
+      <div
+        v-for="alert in activeAlerts"
+        :key="alert.id"
+        :class="['alert-banner', `alert-${alert.severity}`]"
+      >
+        <div class="alert-content">
+          <div class="alert-icon">{{ alert.severity === 'critical' ? '🔴' : '⚠️' }}</div>
+          <div class="alert-text">
+            <div class="alert-title">{{ alert.title }}</div>
+            <div class="alert-message">{{ alert.message }}</div>
+            <div class="alert-time">{{ formatTime(alert.created_at) }}</div>
+          </div>
+        </div>
+        <div class="alert-actions">
+          <button
+            v-if="!alert.acknowledged"
+            @click="acknowledgeAlert(alert.id)"
+            class="btn-acknowledge"
+          >
+            Acknowledge
+          </button>
+          <button @click="dismissAlert(alert.id)" class="btn-dismiss">
+            ✕
+          </button>
+        </div>
+      </div>
+    </div>
+
+    <!-- Page Header -->
     <div class="page-header">
     <div class="page-header">
-      <h1>Superadmin Dashboard</h1>
-      <p>System overview and statistics</p>
+      <h1>System Monitoring Dashboard</h1>
+      <p>Real-time host metrics and device status</p>
     </div>
     </div>
 
 
+    <!-- Device Statistics -->
     <div class="stats-grid">
     <div class="stats-grid">
       <div class="stat-card">
       <div class="stat-card">
-        <div class="stat-icon">🏢</div>
+        <div class="stat-icon">📡</div>
         <div class="stat-content">
         <div class="stat-content">
-          <div class="stat-value">-</div>
-          <div class="stat-label">Organizations</div>
+          <div class="stat-value">{{ deviceStats.total }}</div>
+          <div class="stat-label">Total Devices</div>
         </div>
         </div>
       </div>
       </div>
 
 
-      <div class="stat-card">
-        <div class="stat-icon">📡</div>
+      <div class="stat-card stat-online">
+        <div class="stat-icon"></div>
         <div class="stat-content">
         <div class="stat-content">
-          <div class="stat-value">-</div>
-          <div class="stat-label">Devices</div>
+          <div class="stat-value">{{ deviceStats.online }}</div>
+          <div class="stat-label">Online</div>
         </div>
         </div>
       </div>
       </div>
 
 
-      <div class="stat-card">
-        <div class="stat-icon">👥</div>
+      <div class="stat-card stat-offline">
+        <div class="stat-icon"></div>
         <div class="stat-content">
         <div class="stat-content">
-          <div class="stat-value">-</div>
-          <div class="stat-label">Users</div>
+          <div class="stat-value">{{ deviceStats.offline }}</div>
+          <div class="stat-label">Offline</div>
         </div>
         </div>
       </div>
       </div>
 
 
-      <div class="stat-card">
-        <div class="stat-icon">📊</div>
+      <div class="stat-card stat-error">
+        <div class="stat-icon"></div>
         <div class="stat-content">
         <div class="stat-content">
-          <div class="stat-value">-</div>
-          <div class="stat-label">Events Today</div>
+          <div class="stat-value">{{ deviceStats.error }}</div>
+          <div class="stat-label">Error</div>
         </div>
         </div>
       </div>
       </div>
     </div>
     </div>
 
 
-    <div class="content-section">
-      <h2>Quick Actions</h2>
-      <div class="actions-grid">
-        <router-link to="/superadmin/organizations" class="action-card">
-          <span class="action-icon">🏢</span>
-          <span class="action-title">Manage Organizations</span>
-          <span class="action-desc">Create and configure organizations</span>
-        </router-link>
-
-        <router-link to="/superadmin/devices" class="action-card">
-          <span class="action-icon">📡</span>
-          <span class="action-title">Manage Devices</span>
-          <span class="action-desc">View and assign devices</span>
-        </router-link>
-
-        <router-link to="/superadmin/users" class="action-card">
-          <span class="action-icon">👥</span>
-          <span class="action-title">Manage Users</span>
-          <span class="action-desc">View all system users</span>
-        </router-link>
+    <!-- Host Metrics Charts -->
+    <div class="metrics-section">
+      <h2>Host Metrics</h2>
+
+      <div class="charts-grid">
+        <!-- CPU Chart -->
+        <div class="chart-card">
+          <h3>CPU Usage</h3>
+          <Line v-if="cpuChartData" :data="cpuChartData" :options="cpuChartOptions" />
+          <div v-else class="chart-loading">Loading...</div>
+        </div>
+
+        <!-- Memory Chart -->
+        <div class="chart-card">
+          <h3>Memory Usage</h3>
+          <Line v-if="memoryChartData" :data="memoryChartData" :options="memoryChartOptions" />
+          <div v-else class="chart-loading">Loading...</div>
+        </div>
+
+        <!-- Load Average Chart -->
+        <div class="chart-card">
+          <h3>Load Average</h3>
+          <Line v-if="loadChartData" :data="loadChartData" :options="loadChartOptions" />
+          <div v-else class="chart-loading">Loading...</div>
+        </div>
+
+        <!-- Disk I/O Chart -->
+        <div class="chart-card">
+          <h3>Disk I/O (IOPS)</h3>
+          <Line v-if="diskChartData" :data="diskChartData" :options="diskChartOptions" />
+          <div v-else class="chart-loading">Loading...</div>
+        </div>
+
+        <!-- Network Chart -->
+        <div class="chart-card">
+          <h3>Network Throughput (MB/s)</h3>
+          <Line v-if="networkChartData" :data="networkChartData" :options="networkChartOptions" />
+          <div v-else class="chart-loading">Loading...</div>
+        </div>
+
+        <!-- Disk Usage Chart -->
+        <div class="chart-card">
+          <h3>Disk Usage</h3>
+          <Line v-if="diskUsageChartData" :data="diskUsageChartData" :options="diskUsageChartOptions" />
+          <div v-else class="chart-loading">Loading...</div>
+        </div>
       </div>
       </div>
     </div>
     </div>
   </div>
   </div>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-// Stats will be loaded from API later
+import { ref, onMounted, onUnmounted } from 'vue'
+import { Line } from 'vue-chartjs'
+import {
+  Chart as ChartJS,
+  CategoryScale,
+  LinearScale,
+  PointElement,
+  LineElement,
+  Title,
+  Tooltip,
+  Legend,
+  Filler
+} from 'chart.js'
+import axios from '@/api/client'
+
+// Register Chart.js components
+ChartJS.register(
+  CategoryScale,
+  LinearScale,
+  PointElement,
+  LineElement,
+  Title,
+  Tooltip,
+  Legend,
+  Filler
+)
+
+// Data
+const activeAlerts = ref([])
+const deviceStats = ref({ total: 0, online: 0, offline: 0, error: 0 })
+const hostMetrics = ref([])
+
+// Chart data
+const cpuChartData = ref(null)
+const memoryChartData = ref(null)
+const loadChartData = ref(null)
+const diskChartData = ref(null)
+const networkChartData = ref(null)
+const diskUsageChartData = ref(null)
+
+// Chart options with red threshold highlighting
+const createChartOptions = (title, yMax = 100, thresholdValue = null, unit = '%') => ({
+  responsive: true,
+  maintainAspectRatio: false,
+  plugins: {
+    legend: {
+      display: true,
+      position: 'top'
+    },
+    tooltip: {
+      mode: 'index',
+      intersect: false,
+      callbacks: {
+        label: (context) => {
+          return `${context.dataset.label}: ${context.parsed.y.toFixed(2)}${unit}`
+        }
+      }
+    }
+  },
+  scales: {
+    y: {
+      beginAtZero: true,
+      max: yMax,
+      ticks: {
+        callback: (value) => `${value}${unit}`
+      }
+    },
+    x: {
+      ticks: {
+        maxRotation: 45,
+        minRotation: 45
+      }
+    }
+  },
+  elements: {
+    line: {
+      tension: 0.4
+    }
+  }
+})
+
+const cpuChartOptions = createChartOptions('CPU Usage', 100, 90, '%')
+const memoryChartOptions = createChartOptions('Memory Usage', 100, 90, '%')
+const loadChartOptions = createChartOptions('Load Average', null, null, '')
+const diskChartOptions = createChartOptions('Disk IOPS', null, null, ' IOPS')
+const networkChartOptions = createChartOptions('Network', null, null, ' MB/s')
+const diskUsageChartOptions = createChartOptions('Disk Usage', 100, 90, '%')
+
+let pollingInterval = null
+
+// Functions
+async function loadAlerts() {
+  try {
+    const { data } = await axios.get('/superadmin/monitoring/alerts', {
+      params: { limit: 10, dismissed: false }
+    })
+    activeAlerts.value = data.alerts
+  } catch (error) {
+    console.error('Failed to load alerts:', error)
+  }
+}
+
+async function acknowledgeAlert(alertId) {
+  try {
+    await axios.post(`/superadmin/monitoring/alerts/${alertId}/acknowledge`)
+    await loadAlerts()
+  } catch (error) {
+    console.error('Failed to acknowledge alert:', error)
+  }
+}
+
+async function dismissAlert(alertId) {
+  try {
+    await axios.post(`/superadmin/monitoring/alerts/${alertId}/dismiss`)
+    activeAlerts.value = activeAlerts.value.filter(a => a.id !== alertId)
+  } catch (error) {
+    console.error('Failed to dismiss alert:', error)
+  }
+}
+
+async function loadDeviceStats() {
+  try {
+    const { data } = await axios.get('/superadmin/devices')
+    const devices = data.devices
+
+    deviceStats.value = {
+      total: devices.length,
+      online: devices.filter(d => d.status === 'online').length,
+      offline: devices.filter(d => d.status === 'offline').length,
+      error: devices.filter(d => d.status === 'error').length
+    }
+  } catch (error) {
+    console.error('Failed to load device stats:', error)
+  }
+}
+
+async function loadHostMetrics() {
+  try {
+    const { data } = await axios.get('/superadmin/monitoring/host-metrics/recent', {
+      params: { limit: 30 }
+    })
+    hostMetrics.value = data.metrics.reverse() // Oldest first
+    updateCharts()
+  } catch (error) {
+    console.error('Failed to load host metrics:', error)
+  }
+}
+
+function updateCharts() {
+  if (hostMetrics.value.length === 0) return
+
+  const labels = hostMetrics.value.map(m =>
+    new Date(m.timestamp).toLocaleTimeString('en-US', {
+      hour: '2-digit',
+      minute: '2-digit'
+    })
+  )
+
+  // CPU Chart
+  cpuChartData.value = {
+    labels,
+    datasets: [
+      {
+        label: 'CPU %',
+        data: hostMetrics.value.map(m => m.cpu_percent),
+        borderColor: 'rgb(99, 102, 241)',
+        backgroundColor: 'rgba(99, 102, 241, 0.1)',
+        fill: true,
+        segment: {
+          borderColor: ctx => {
+            const value = ctx.p1.parsed.y
+            return value >= 90 ? 'rgb(239, 68, 68)' : 'rgb(99, 102, 241)'
+          }
+        }
+      }
+    ]
+  }
+
+  // Memory Chart
+  memoryChartData.value = {
+    labels,
+    datasets: [
+      {
+        label: 'Memory %',
+        data: hostMetrics.value.map(m => m.memory_percent),
+        borderColor: 'rgb(34, 197, 94)',
+        backgroundColor: 'rgba(34, 197, 94, 0.1)',
+        fill: true,
+        segment: {
+          borderColor: ctx => {
+            const value = ctx.p1.parsed.y
+            return value >= 90 ? 'rgb(239, 68, 68)' : 'rgb(34, 197, 94)'
+          }
+        }
+      }
+    ]
+  }
+
+  // Load Average Chart
+  const cpuCount = hostMetrics.value[0]?.cpu_count || 1
+  loadChartData.value = {
+    labels,
+    datasets: [
+      {
+        label: 'Load 1m',
+        data: hostMetrics.value.map(m => m.load_1),
+        borderColor: 'rgb(244, 114, 182)',
+        backgroundColor: 'rgba(244, 114, 182, 0.1)',
+        fill: false
+      },
+      {
+        label: 'Load 5m',
+        data: hostMetrics.value.map(m => m.load_5),
+        borderColor: 'rgb(251, 146, 60)',
+        backgroundColor: 'rgba(251, 146, 60, 0.1)',
+        fill: false
+      },
+      {
+        label: 'Load 15m',
+        data: hostMetrics.value.map(m => m.load_15),
+        borderColor: 'rgb(234, 179, 8)',
+        backgroundColor: 'rgba(234, 179, 8, 0.1)',
+        fill: false
+      },
+      {
+        label: `Threshold (${cpuCount} cores)`,
+        data: Array(labels.length).fill(cpuCount),
+        borderColor: 'rgba(239, 68, 68, 0.5)',
+        borderDash: [5, 5],
+        fill: false,
+        pointRadius: 0
+      }
+    ]
+  }
+
+  // Disk I/O Chart (IOPS)
+  diskChartData.value = {
+    labels,
+    datasets: [
+      {
+        label: 'Read IOPS',
+        data: hostMetrics.value.map(m => m.disk_read_iops),
+        borderColor: 'rgb(59, 130, 246)',
+        backgroundColor: 'rgba(59, 130, 246, 0.1)',
+        fill: false
+      },
+      {
+        label: 'Write IOPS',
+        data: hostMetrics.value.map(m => m.disk_write_iops),
+        borderColor: 'rgb(168, 85, 247)',
+        backgroundColor: 'rgba(168, 85, 247, 0.1)',
+        fill: false
+      }
+    ]
+  }
+
+  // Network Chart (MB/s)
+  networkChartData.value = {
+    labels,
+    datasets: [
+      {
+        label: 'In MB/s',
+        data: hostMetrics.value.map(m => m.net_in_mbps),
+        borderColor: 'rgb(14, 165, 233)',
+        backgroundColor: 'rgba(14, 165, 233, 0.1)',
+        fill: false
+      },
+      {
+        label: 'Out MB/s',
+        data: hostMetrics.value.map(m => m.net_out_mbps),
+        borderColor: 'rgb(245, 158, 11)',
+        backgroundColor: 'rgba(245, 158, 11, 0.1)',
+        fill: false
+      }
+    ]
+  }
+
+  // Disk Usage Chart
+  diskUsageChartData.value = {
+    labels,
+    datasets: [
+      {
+        label: 'Disk Usage %',
+        data: hostMetrics.value.map(m => m.disk_usage_percent),
+        borderColor: 'rgb(236, 72, 153)',
+        backgroundColor: 'rgba(236, 72, 153, 0.1)',
+        fill: true,
+        segment: {
+          borderColor: ctx => {
+            const value = ctx.p1.parsed.y
+            return value >= 90 ? 'rgb(239, 68, 68)' : 'rgb(236, 72, 153)'
+          }
+        }
+      }
+    ]
+  }
+}
+
+function formatTime(timestamp) {
+  const date = new Date(timestamp)
+  const now = new Date()
+  const diffMs = now - date
+  const diffMins = Math.floor(diffMs / 60000)
+
+  if (diffMins < 1) return 'just now'
+  if (diffMins < 60) return `${diffMins} minutes ago`
+  const diffHours = Math.floor(diffMins / 60)
+  if (diffHours < 24) return `${diffHours} hours ago`
+  return date.toLocaleString()
+}
+
+async function refreshData() {
+  await Promise.all([
+    loadAlerts(),
+    loadDeviceStats(),
+    loadHostMetrics()
+  ])
+}
+
+// Lifecycle
+onMounted(async () => {
+  await refreshData()
+  // Poll every 30 seconds
+  pollingInterval = setInterval(refreshData, 30000)
+})
+
+onUnmounted(() => {
+  if (pollingInterval) {
+    clearInterval(pollingInterval)
+  }
+})
 </script>
 </script>
 
 
 <style scoped>
 <style scoped>
 .dashboard {
 .dashboard {
   padding: 32px;
   padding: 32px;
+  max-width: 1600px;
+  margin: 0 auto;
+}
+
+/* Alert Banner */
+.alerts-section {
+  margin-bottom: 24px;
+}
+
+.alert-banner {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 16px 20px;
+  border-radius: 8px;
+  margin-bottom: 12px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.alert-critical {
+  background: #fee2e2;
+  border-left: 4px solid #dc2626;
+}
+
+.alert-warning {
+  background: #fef3c7;
+  border-left: 4px solid #f59e0b;
+}
+
+.alert-info {
+  background: #dbeafe;
+  border-left: 4px solid #3b82f6;
 }
 }
 
 
+.alert-content {
+  display: flex;
+  align-items: flex-start;
+  gap: 12px;
+  flex: 1;
+}
+
+.alert-icon {
+  font-size: 24px;
+  flex-shrink: 0;
+}
+
+.alert-text {
+  flex: 1;
+}
+
+.alert-title {
+  font-weight: 600;
+  font-size: 16px;
+  color: #1a202c;
+  margin-bottom: 4px;
+}
+
+.alert-message {
+  font-size: 14px;
+  color: #4a5568;
+  margin-bottom: 4px;
+}
+
+.alert-time {
+  font-size: 12px;
+  color: #718096;
+}
+
+.alert-actions {
+  display: flex;
+  gap: 8px;
+  align-items: center;
+}
+
+.btn-acknowledge {
+  padding: 8px 16px;
+  background: white;
+  border: 1px solid #d1d5db;
+  border-radius: 6px;
+  font-size: 14px;
+  cursor: pointer;
+  transition: all 0.2s;
+}
+
+.btn-acknowledge:hover {
+  background: #f9fafb;
+  border-color: #9ca3af;
+}
+
+.btn-dismiss {
+  padding: 8px 12px;
+  background: transparent;
+  border: none;
+  font-size: 18px;
+  cursor: pointer;
+  color: #6b7280;
+  transition: color 0.2s;
+}
+
+.btn-dismiss:hover {
+  color: #1f2937;
+}
+
+/* Page Header */
 .page-header {
 .page-header {
   margin-bottom: 32px;
   margin-bottom: 32px;
 }
 }
@@ -89,6 +579,7 @@
   font-size: 16px;
   font-size: 16px;
 }
 }
 
 
+/* Device Stats */
 .stats-grid {
 .stats-grid {
   display: grid;
   display: grid;
   grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
   grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
@@ -104,6 +595,24 @@
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   gap: 16px;
   gap: 16px;
+  transition: transform 0.2s;
+}
+
+.stat-card:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+}
+
+.stat-online {
+  border-left: 4px solid #10b981;
+}
+
+.stat-offline {
+  border-left: 4px solid #6b7280;
+}
+
+.stat-error {
+  border-left: 4px solid #ef4444;
 }
 }
 
 
 .stat-icon {
 .stat-icon {
@@ -115,7 +624,7 @@
 }
 }
 
 
 .stat-value {
 .stat-value {
-  font-size: 28px;
+  font-size: 32px;
   font-weight: 700;
   font-weight: 700;
   color: #1a202c;
   color: #1a202c;
   margin-bottom: 4px;
   margin-bottom: 4px;
@@ -124,59 +633,54 @@
 .stat-label {
 .stat-label {
   font-size: 14px;
   font-size: 14px;
   color: #718096;
   color: #718096;
+  font-weight: 500;
 }
 }
 
 
-.content-section {
+/* Metrics Section */
+.metrics-section {
   background: white;
   background: white;
   border-radius: 12px;
   border-radius: 12px;
   padding: 24px;
   padding: 24px;
   box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
   box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
 }
 }
 
 
-.content-section h2 {
-  font-size: 20px;
+.metrics-section h2 {
+  font-size: 24px;
   font-weight: 600;
   font-weight: 600;
   color: #1a202c;
   color: #1a202c;
-  margin-bottom: 20px;
+  margin-bottom: 24px;
 }
 }
 
 
-.actions-grid {
+.charts-grid {
   display: grid;
   display: grid;
-  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
-  gap: 16px;
+  grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
+  gap: 24px;
 }
 }
 
 
-.action-card {
-  display: flex;
-  flex-direction: column;
-  align-items: flex-start;
-  padding: 20px;
-  border: 1px solid #e2e8f0;
+.chart-card {
+  background: #f9fafb;
   border-radius: 8px;
   border-radius: 8px;
-  text-decoration: none;
-  transition: all 0.2s;
-}
-
-.action-card:hover {
-  border-color: #667eea;
-  transform: translateY(-2px);
-  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);
-}
-
-.action-icon {
-  font-size: 32px;
-  margin-bottom: 12px;
+  padding: 20px;
+  min-height: 300px;
 }
 }
 
 
-.action-title {
+.chart-card h3 {
   font-size: 16px;
   font-size: 16px;
   font-weight: 600;
   font-weight: 600;
-  color: #1a202c;
-  margin-bottom: 4px;
+  color: #374151;
+  margin-bottom: 16px;
+}
+
+.chart-card canvas {
+  height: 250px !important;
 }
 }
 
 
-.action-desc {
+.chart-loading {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 250px;
+  color: #9ca3af;
   font-size: 14px;
   font-size: 14px;
-  color: #718096;
 }
 }
 </style>
 </style>