Add analytics dashboard view and API integration

- Add AnalyticsView.vue for real-time API analytics - Update router to
include /analytics route - Add Analytics link to BottomNav - Improve
MetricCard layout for dashboard consistency - Update main.ts to
initialize global auth store - Add Dockerfile and .env for
containerization and config - Update README with complete API and
architecture overview - Disable Tailwind in main.scss for SCSS-only
styling
This commit is contained in:
rafaeldpsilva
2025-09-18 14:28:01 +01:00
parent 05baaca23c
commit 32c63628b6
9 changed files with 759 additions and 5 deletions

View File

@@ -1,4 +1,4 @@
@use 'tailwindcss';
/* @use 'tailwindcss';
@import 'abstracts/variables';
@import 'abstracts/mixins';
@@ -20,4 +20,4 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@tailwind utilities; */

View File

@@ -1,5 +1,5 @@
<template>
<div class="bg-white rounded-2xl shadow-sm flex flex-col justify-between aspect-square p-4">
<div class="bg-white rounded-2xl shadow-sm flex flex-col justify-between h-full w-full p-4">
<h6 class="text-sm font-bold text-gray-500">{{ title }}</h6>
<div class="flex-grow flex items-center justify-start">
<p class="text-gray-900 font-bold text-2xl">

View File

@@ -73,6 +73,27 @@
<span class="text-xs">AI Optimize</span>
</router-link>
</li>
<li>
<router-link
to="/analytics"
class="flex flex-col items-center font-medium"
:class="
$route.name === 'analytics'
? 'text-indigo-600'
: 'text-gray-600 hover:text-indigo-600'
"
>
<svg class="w-6 h-6 mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v14a2 2 0 002 2z"
/>
</svg>
<span class="text-xs">Analytics</span>
</router-link>
</li>
<li>
<router-link
to="/settings"

View File

@@ -8,9 +8,33 @@ import App from './App.vue'
import router from './router'
const app = createApp(App)
const pinia = createPinia()
app.use(createPinia())
app.use(pinia)
app.use(router)
// Initialize auth store and make it globally available for API client
import { useAuthStore } from './stores/auth'
app.component('v-chart', ECharts)
app.mount('#app')
// Mount the app
const mountedApp = app.mount('#app')
// Make auth store globally available after app is mounted
setTimeout(() => {
try {
const authStore = useAuthStore()
;(window as any).__AUTH_STORE__ = authStore
// Ensure authentication on app start
authStore.ensureAuthenticated().then((success) => {
if (success) {
console.log('Authentication initialized successfully')
} else {
console.warn('Failed to initialize authentication')
}
})
} catch (error) {
console.error('Failed to initialize auth store:', error)
}
}, 100)

View File

@@ -3,6 +3,7 @@ import HomeView from '../views/HomeView.vue'
import SensorManagementView from '../views/SensorManagementView.vue'
import AIOptimizationView from '../views/AIOptimizationView.vue'
import SettingsView from '../views/SettingsView.vue'
import AnalyticsView from '../views/AnalyticsView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@@ -27,6 +28,11 @@ const router = createRouter({
name: 'settings',
component: SettingsView,
},
{
path: '/analytics',
name: 'analytics',
component: AnalyticsView,
},
],
})

267
src/views/AnalyticsView.vue Normal file
View File

@@ -0,0 +1,267 @@
<template>
<div class="min-h-screen bg-gray-50">
<div class="px-4 py-6 mx-auto max-w-7xl sm:px-6 lg:px-8">
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900">API Dashboard</h1>
<p class="text-gray-600 mt-2">Real-time data from backend APIs</p>
</div>
<!-- API Status Section -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center">
<div class="p-2 bg-blue-100 rounded-lg">
<svg class="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">System Status</p>
<p class="text-lg font-semibold" :class="healthStatus?.status === 'healthy' ? 'text-green-600' : 'text-red-600'">
{{ healthStatus?.status || 'Unknown' }}
</p>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center">
<div class="p-2 bg-green-100 rounded-lg">
<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">Total Sensors</p>
<p class="text-lg font-semibold text-gray-900">{{ healthStatus?.total_sensors || 0 }}</p>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center">
<div class="p-2 bg-yellow-100 rounded-lg">
<svg class="w-6 h-6 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">Active Sensors</p>
<p class="text-lg font-semibold text-gray-900">{{ healthStatus?.active_sensors || 0 }}</p>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center">
<div class="p-2 bg-purple-100 rounded-lg">
<svg class="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
</svg>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">Total Readings</p>
<p class="text-lg font-semibold text-gray-900">{{ formatNumber(healthStatus?.total_readings || 0) }}</p>
</div>
</div>
</div>
</div>
<!-- Loading States -->
<div v-if="energyStore.apiLoading" class="bg-white rounded-lg shadow p-6 mb-8">
<div class="flex items-center justify-center">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<p class="ml-3 text-gray-600">Loading API data...</p>
</div>
</div>
<!-- Error States -->
<div v-if="energyStore.apiError" class="bg-red-50 border border-red-200 rounded-lg p-4 mb-8">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800">API Error</h3>
<p class="mt-1 text-sm text-red-700">{{ energyStore.apiError }}</p>
</div>
</div>
</div>
<!-- Analytics Summary -->
<div v-if="analyticsData.summary" class="bg-white rounded-lg shadow p-6 mb-8">
<h2 class="text-xl font-semibold text-gray-900 mb-4">Analytics Summary (Last 24 Hours)</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<p class="text-sm font-medium text-gray-600 mb-2">Total Energy Consumption</p>
<p class="text-2xl font-bold text-blue-600">
{{ analyticsData.summary.total_energy_consumption.value.toFixed(2) }}
{{ analyticsData.summary.total_energy_consumption.unit }}
</p>
</div>
<div>
<p class="text-sm font-medium text-gray-600 mb-2">Average Power</p>
<p class="text-2xl font-bold text-green-600">
{{ analyticsData.summary.average_power.value.toFixed(2) }}
{{ analyticsData.summary.average_power.unit }}
</p>
</div>
<div>
<p class="text-sm font-medium text-gray-600 mb-2">Peak Power</p>
<p class="text-2xl font-bold text-red-600">
{{ analyticsData.summary.peak_power.value.toFixed(2) }}
{{ analyticsData.summary.peak_power.unit }}
</p>
<p class="text-xs text-gray-500 mt-1">
at {{ new Date(analyticsData.summary.peak_power.timestamp * 1000).toLocaleString() }}
</p>
</div>
</div>
</div>
<!-- Sensors Section -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
<div class="bg-white rounded-lg shadow">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-lg font-semibold text-gray-900">API Sensors</h2>
</div>
<div class="p-6">
<div v-if="apiSensors.length === 0" class="text-center text-gray-500 py-8">
No sensors found from API
</div>
<div v-else class="space-y-3">
<div v-for="sensor in apiSensors" :key="sensor.sensor_id"
class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div>
<p class="font-medium text-gray-900">{{ sensor.sensor_id }}</p>
<p class="text-sm text-gray-500">{{ sensor.room || 'No room assigned' }}</p>
<p class="text-xs text-gray-400">{{ sensor.sensor_type }} {{ sensor.total_readings }} readings</p>
</div>
<div class="flex items-center">
<span :class="[
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium',
sensor.status === 'online' ? 'bg-green-100 text-green-800' :
sensor.status === 'offline' ? 'bg-red-100 text-red-800' :
'bg-yellow-100 text-yellow-800'
]">
{{ sensor.status }}
</span>
</div>
</div>
</div>
</div>
</div>
<!-- Rooms Section -->
<div class="bg-white rounded-lg shadow">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-lg font-semibold text-gray-900">API Rooms</h2>
</div>
<div class="p-6">
<div v-if="apiRooms.length === 0" class="text-center text-gray-500 py-8">
No rooms found from API
</div>
<div v-else class="space-y-3">
<div v-for="room in apiRooms" :key="room.room"
class="p-3 bg-gray-50 rounded-lg">
<div class="flex items-center justify-between mb-2">
<p class="font-medium text-gray-900">{{ room.room }}</p>
<span class="text-sm text-gray-500">{{ room.sensor_count }} sensors</span>
</div>
<div class="text-xs text-gray-600 space-y-1">
<p>Types: {{ room.sensor_types.join(', ') }}</p>
<div v-if="room.latest_metrics">
<span v-if="room.latest_metrics.energy" class="mr-4">
Energy: {{ room.latest_metrics.energy.current }} {{ room.latest_metrics.energy.unit }}
</span>
<span v-if="room.latest_metrics.co2">
CO2: {{ room.latest_metrics.co2.current }} {{ room.latest_metrics.co2.unit }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="bg-white rounded-lg shadow p-6">
<h2 class="text-lg font-semibold text-gray-900 mb-4">API Actions</h2>
<div class="flex flex-wrap gap-3">
<button @click="refreshAllData"
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
:disabled="energyStore.apiLoading">
<svg v-if="!energyStore.apiLoading" class="-ml-1 mr-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
<div v-else class="animate-spin -ml-1 mr-2 h-4 w-4 border-2 border-white border-t-transparent rounded-full"></div>
Refresh All Data
</button>
<button @click="fetchSensorsOnly"
class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
:disabled="energyStore.apiLoading">
Fetch Sensors
</button>
<button @click="fetchRoomsOnly"
class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
:disabled="energyStore.apiLoading">
Fetch Rooms
</button>
<button @click="fetchAnalyticsOnly"
class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
:disabled="energyStore.apiLoading">
Fetch Analytics
</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted } from 'vue'
import { useEnergyStore } from '@/stores/energy'
const energyStore = useEnergyStore()
// Computed properties
const apiSensors = computed(() => energyStore.apiSensors)
const apiRooms = computed(() => energyStore.apiRooms)
const analyticsData = computed(() => energyStore.analyticsData)
const healthStatus = computed(() => energyStore.healthStatus)
// Helper functions
const formatNumber = (num: number): string => {
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M'
if (num >= 1000) return (num / 1000).toFixed(1) + 'K'
return num.toString()
}
// Action functions
const refreshAllData = async () => {
await energyStore.initializeFromApi()
}
const fetchSensorsOnly = async () => {
await energyStore.fetchApiSensors()
}
const fetchRoomsOnly = async () => {
await energyStore.fetchApiRooms()
}
const fetchAnalyticsOnly = async () => {
await energyStore.fetchAnalyticsSummary()
}
// Initialize data on mount
onMounted(async () => {
await energyStore.initializeFromApi()
})
</script>