Add API service layer, authentication store, and composables
- Implement API service modules for sensors, rooms, analytics, health, and auth - Add Pinia auth store for JWT token management and validation - Create Vue composables for API integration and state management - Update settings and AI optimization views for code style and connection URLs - Add test-websocket.html for local WebSocket testing
This commit is contained in:
119
src/components/common/AuthStatus.vue
Normal file
119
src/components/common/AuthStatus.vue
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<template>
|
||||||
|
<div class="auth-status" :class="authStatusClass">
|
||||||
|
<div class="auth-status__indicator">
|
||||||
|
<span class="auth-status__dot" :class="statusDotClass"></span>
|
||||||
|
<span class="auth-status__text">{{ statusText }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="authStore.timeUntilExpiry" class="auth-status__expiry">
|
||||||
|
Token expires in: {{ formatTimeUntilExpiry() }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="authStore.error" class="auth-status__error">
|
||||||
|
Auth Error: {{ authStore.error }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="!authStore.isAuthenticated"
|
||||||
|
@click="handleReauth"
|
||||||
|
:disabled="authStore.isLoading"
|
||||||
|
class="auth-status__retry-btn"
|
||||||
|
>
|
||||||
|
{{ authStore.isLoading ? 'Authenticating...' : 'Retry Authentication' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
const authStatusClass = computed(() => ({
|
||||||
|
'auth-status--authenticated': authStore.isAuthenticated,
|
||||||
|
'auth-status--error': !authStore.isAuthenticated || authStore.error,
|
||||||
|
'auth-status--loading': authStore.isLoading
|
||||||
|
}))
|
||||||
|
|
||||||
|
const statusDotClass = computed(() => ({
|
||||||
|
'auth-status__dot--green': authStore.isAuthenticated && !authStore.error,
|
||||||
|
'auth-status__dot--red': !authStore.isAuthenticated || authStore.error,
|
||||||
|
'auth-status__dot--yellow': authStore.isLoading
|
||||||
|
}))
|
||||||
|
|
||||||
|
const statusText = computed(() => {
|
||||||
|
if (authStore.isLoading) return 'Authenticating...'
|
||||||
|
if (authStore.isAuthenticated) return 'Authenticated'
|
||||||
|
return 'Not authenticated'
|
||||||
|
})
|
||||||
|
|
||||||
|
function formatTimeUntilExpiry(): string {
|
||||||
|
const time = authStore.timeUntilExpiry
|
||||||
|
if (!time) return 'Unknown'
|
||||||
|
|
||||||
|
if (time.hours > 0) {
|
||||||
|
return `${time.hours}h ${time.minutes}m`
|
||||||
|
} else {
|
||||||
|
return `${time.minutes}m`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleReauth() {
|
||||||
|
await authStore.generateToken()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.auth-status {
|
||||||
|
@apply text-xs bg-gray-50 p-2 rounded border;
|
||||||
|
|
||||||
|
&--authenticated {
|
||||||
|
@apply bg-green-50 border-green-200 text-green-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--error {
|
||||||
|
@apply bg-red-50 border-red-200 text-red-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--loading {
|
||||||
|
@apply bg-yellow-50 border-yellow-200 text-yellow-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__indicator {
|
||||||
|
@apply flex items-center gap-1 mb-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__dot {
|
||||||
|
@apply w-2 h-2 rounded-full;
|
||||||
|
|
||||||
|
&--green {
|
||||||
|
@apply bg-green-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--red {
|
||||||
|
@apply bg-red-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--yellow {
|
||||||
|
@apply bg-yellow-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
@apply font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__expiry {
|
||||||
|
@apply text-gray-600 mb-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__error {
|
||||||
|
@apply text-red-600 text-xs mb-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__retry-btn {
|
||||||
|
@apply bg-blue-500 text-white px-2 py-1 rounded text-xs hover:bg-blue-600 disabled:opacity-50;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
341
src/composables/useApi.ts
Normal file
341
src/composables/useApi.ts
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
/**
|
||||||
|
* Vue Composable for API Integration
|
||||||
|
* Provides reactive API state management
|
||||||
|
*/
|
||||||
|
import { ref, reactive } from 'vue'
|
||||||
|
import {
|
||||||
|
sensorsApi,
|
||||||
|
roomsApi,
|
||||||
|
analyticsApi,
|
||||||
|
healthApi,
|
||||||
|
type SensorInfo,
|
||||||
|
type RoomInfo,
|
||||||
|
type RoomData,
|
||||||
|
type AnalyticsSummary,
|
||||||
|
type EnergyTrends,
|
||||||
|
type RoomComparison,
|
||||||
|
type SystemEvent,
|
||||||
|
type HealthCheck,
|
||||||
|
type SystemStatus,
|
||||||
|
type DataQuery,
|
||||||
|
type DataResponse
|
||||||
|
} from '@/services'
|
||||||
|
|
||||||
|
interface ApiState {
|
||||||
|
loading: boolean
|
||||||
|
error: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useApi() {
|
||||||
|
// Global API state
|
||||||
|
const globalState = reactive<ApiState>({
|
||||||
|
loading: false,
|
||||||
|
error: null
|
||||||
|
})
|
||||||
|
|
||||||
|
// Helper to handle API calls with state management
|
||||||
|
async function handleApiCall<T>(
|
||||||
|
apiCall: () => Promise<T>,
|
||||||
|
localState?: { loading: boolean; error: string | null }
|
||||||
|
): Promise<T | null> {
|
||||||
|
const state = localState || globalState
|
||||||
|
|
||||||
|
state.loading = true
|
||||||
|
state.error = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await apiCall()
|
||||||
|
return result
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||||
|
state.error = errorMessage
|
||||||
|
console.error('API call failed:', errorMessage)
|
||||||
|
return null
|
||||||
|
} finally {
|
||||||
|
state.loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
globalState,
|
||||||
|
handleApiCall
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sensors API composable
|
||||||
|
export function useSensorsApi() {
|
||||||
|
const state = reactive<ApiState>({
|
||||||
|
loading: false,
|
||||||
|
error: null
|
||||||
|
})
|
||||||
|
|
||||||
|
const sensors = ref<SensorInfo[]>([])
|
||||||
|
const currentSensor = ref<SensorInfo | null>(null)
|
||||||
|
const sensorData = ref<DataResponse | null>(null)
|
||||||
|
|
||||||
|
const { handleApiCall } = useApi()
|
||||||
|
|
||||||
|
const fetchSensors = async (params?: {
|
||||||
|
room?: string
|
||||||
|
sensor_type?: any
|
||||||
|
status?: any
|
||||||
|
}) => {
|
||||||
|
const result = await handleApiCall(
|
||||||
|
() => sensorsApi.getSensors(params),
|
||||||
|
state
|
||||||
|
)
|
||||||
|
if (result) {
|
||||||
|
sensors.value = result
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchSensor = async (sensorId: string) => {
|
||||||
|
const result = await handleApiCall(
|
||||||
|
() => sensorsApi.getSensor(sensorId),
|
||||||
|
state
|
||||||
|
)
|
||||||
|
if (result) {
|
||||||
|
currentSensor.value = result
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchSensorData = async (
|
||||||
|
sensorId: string,
|
||||||
|
params?: {
|
||||||
|
start_time?: number
|
||||||
|
end_time?: number
|
||||||
|
limit?: number
|
||||||
|
offset?: number
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const result = await handleApiCall(
|
||||||
|
() => sensorsApi.getSensorData(sensorId, params),
|
||||||
|
state
|
||||||
|
)
|
||||||
|
if (result) {
|
||||||
|
sensorData.value = result
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryData = async (query: DataQuery) => {
|
||||||
|
return handleApiCall(
|
||||||
|
() => sensorsApi.queryData(query),
|
||||||
|
state
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateSensorMetadata = async (
|
||||||
|
sensorId: string,
|
||||||
|
metadata: Record<string, any>
|
||||||
|
) => {
|
||||||
|
return handleApiCall(
|
||||||
|
() => sensorsApi.updateSensorMetadata(sensorId, metadata),
|
||||||
|
state
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteSensor = async (sensorId: string) => {
|
||||||
|
return handleApiCall(
|
||||||
|
() => sensorsApi.deleteSensor(sensorId),
|
||||||
|
state
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportData = async (params: {
|
||||||
|
start_time: number
|
||||||
|
end_time: number
|
||||||
|
sensor_ids?: string
|
||||||
|
format?: 'json' | 'csv'
|
||||||
|
}) => {
|
||||||
|
return handleApiCall(
|
||||||
|
() => sensorsApi.exportData(params),
|
||||||
|
state
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
sensors,
|
||||||
|
currentSensor,
|
||||||
|
sensorData,
|
||||||
|
fetchSensors,
|
||||||
|
fetchSensor,
|
||||||
|
fetchSensorData,
|
||||||
|
queryData,
|
||||||
|
updateSensorMetadata,
|
||||||
|
deleteSensor,
|
||||||
|
exportData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rooms API composable
|
||||||
|
export function useRoomsApi() {
|
||||||
|
const state = reactive<ApiState>({
|
||||||
|
loading: false,
|
||||||
|
error: null
|
||||||
|
})
|
||||||
|
|
||||||
|
const rooms = ref<RoomInfo[]>([])
|
||||||
|
const currentRoomData = ref<RoomData | null>(null)
|
||||||
|
|
||||||
|
const { handleApiCall } = useApi()
|
||||||
|
|
||||||
|
const fetchRooms = async () => {
|
||||||
|
const result = await handleApiCall(
|
||||||
|
() => roomsApi.getRooms(),
|
||||||
|
state
|
||||||
|
)
|
||||||
|
if (result) {
|
||||||
|
rooms.value = result
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchRoomData = async (
|
||||||
|
roomName: string,
|
||||||
|
params?: {
|
||||||
|
start_time?: number
|
||||||
|
end_time?: number
|
||||||
|
limit?: number
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const result = await handleApiCall(
|
||||||
|
() => roomsApi.getRoomData(roomName, params),
|
||||||
|
state
|
||||||
|
)
|
||||||
|
if (result) {
|
||||||
|
currentRoomData.value = result
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
rooms,
|
||||||
|
currentRoomData,
|
||||||
|
fetchRooms,
|
||||||
|
fetchRoomData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Analytics API composable
|
||||||
|
export function useAnalyticsApi() {
|
||||||
|
const state = reactive<ApiState>({
|
||||||
|
loading: false,
|
||||||
|
error: null
|
||||||
|
})
|
||||||
|
|
||||||
|
const summary = ref<AnalyticsSummary | null>(null)
|
||||||
|
const trends = ref<EnergyTrends | null>(null)
|
||||||
|
const roomComparison = ref<RoomComparison | null>(null)
|
||||||
|
const events = ref<SystemEvent[]>([])
|
||||||
|
|
||||||
|
const { handleApiCall } = useApi()
|
||||||
|
|
||||||
|
const fetchAnalyticsSummary = async (hours: number = 24) => {
|
||||||
|
const result = await handleApiCall(
|
||||||
|
() => analyticsApi.getAnalyticsSummary(hours),
|
||||||
|
state
|
||||||
|
)
|
||||||
|
if (result) {
|
||||||
|
summary.value = result
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchEnergyTrends = async (hours: number = 168) => {
|
||||||
|
const result = await handleApiCall(
|
||||||
|
() => analyticsApi.getEnergyTrends(hours),
|
||||||
|
state
|
||||||
|
)
|
||||||
|
if (result) {
|
||||||
|
trends.value = result
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchRoomComparison = async (hours: number = 24) => {
|
||||||
|
const result = await handleApiCall(
|
||||||
|
() => analyticsApi.getRoomComparison(hours),
|
||||||
|
state
|
||||||
|
)
|
||||||
|
if (result) {
|
||||||
|
roomComparison.value = result
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchEvents = async (params?: {
|
||||||
|
severity?: string
|
||||||
|
event_type?: string
|
||||||
|
hours?: number
|
||||||
|
limit?: number
|
||||||
|
}) => {
|
||||||
|
const result = await handleApiCall(
|
||||||
|
() => analyticsApi.getEvents(params),
|
||||||
|
state
|
||||||
|
)
|
||||||
|
if (result) {
|
||||||
|
events.value = result.events
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
summary,
|
||||||
|
trends,
|
||||||
|
roomComparison,
|
||||||
|
events,
|
||||||
|
fetchAnalyticsSummary,
|
||||||
|
fetchEnergyTrends,
|
||||||
|
fetchRoomComparison,
|
||||||
|
fetchEvents
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Health API composable
|
||||||
|
export function useHealthApi() {
|
||||||
|
const state = reactive<ApiState>({
|
||||||
|
loading: false,
|
||||||
|
error: null
|
||||||
|
})
|
||||||
|
|
||||||
|
const health = ref<HealthCheck | null>(null)
|
||||||
|
const status = ref<SystemStatus | null>(null)
|
||||||
|
|
||||||
|
const { handleApiCall } = useApi()
|
||||||
|
|
||||||
|
const fetchHealth = async () => {
|
||||||
|
const result = await handleApiCall(
|
||||||
|
() => healthApi.getHealth(),
|
||||||
|
state
|
||||||
|
)
|
||||||
|
if (result) {
|
||||||
|
health.value = result
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchStatus = async () => {
|
||||||
|
const result = await handleApiCall(
|
||||||
|
() => healthApi.getStatus(),
|
||||||
|
state
|
||||||
|
)
|
||||||
|
if (result) {
|
||||||
|
status.value = result
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
health,
|
||||||
|
status,
|
||||||
|
fetchHealth,
|
||||||
|
fetchStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
56
src/services/analyticsApi.ts
Normal file
56
src/services/analyticsApi.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* Analytics API Service
|
||||||
|
* Handles analytics and reporting API calls
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
apiClient,
|
||||||
|
type AnalyticsSummary,
|
||||||
|
type EnergyTrends,
|
||||||
|
type RoomComparison,
|
||||||
|
type SystemEvent,
|
||||||
|
} from './api'
|
||||||
|
|
||||||
|
export const analyticsApi = {
|
||||||
|
/**
|
||||||
|
* Get analytics summary for the specified time period
|
||||||
|
*/
|
||||||
|
async getAnalyticsSummary(hours: number = 24): Promise<AnalyticsSummary> {
|
||||||
|
return apiClient.get<AnalyticsSummary>('/api/v1/analytics/summary', { hours })
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get energy consumption trends
|
||||||
|
*/
|
||||||
|
async getEnergyTrends(hours: number = 168): Promise<EnergyTrends> {
|
||||||
|
return apiClient.get<EnergyTrends>('/api/v1/analytics/trends', { hours })
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get room-by-room comparison analytics
|
||||||
|
*/
|
||||||
|
async getRoomComparison(hours: number = 24): Promise<RoomComparison> {
|
||||||
|
return apiClient.get<RoomComparison>('/api/v1/analytics/rooms', { hours })
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get recent system events and alerts
|
||||||
|
*/
|
||||||
|
async getEvents(params?: {
|
||||||
|
severity?: string
|
||||||
|
event_type?: string
|
||||||
|
hours?: number
|
||||||
|
limit?: number
|
||||||
|
}): Promise<{
|
||||||
|
events: SystemEvent[]
|
||||||
|
count: number
|
||||||
|
period_hours: number
|
||||||
|
}> {
|
||||||
|
return apiClient.get<{
|
||||||
|
events: SystemEvent[]
|
||||||
|
count: number
|
||||||
|
period_hours: number
|
||||||
|
}>('/api/v1/events', params)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default analyticsApi
|
||||||
336
src/services/api.ts
Normal file
336
src/services/api.ts
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
/**
|
||||||
|
* API Service Layer for Energy Monitoring Dashboard
|
||||||
|
* Handles all backend API communications
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Base configuration
|
||||||
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000'
|
||||||
|
|
||||||
|
// API Response types
|
||||||
|
export interface ApiResponse<T = any> {
|
||||||
|
data: T
|
||||||
|
total_count?: number
|
||||||
|
query?: any
|
||||||
|
execution_time_ms?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HealthCheck {
|
||||||
|
status: 'healthy' | 'degraded'
|
||||||
|
mongodb_connected: boolean
|
||||||
|
redis_connected: boolean
|
||||||
|
total_sensors: number
|
||||||
|
active_sensors: number
|
||||||
|
total_readings: number
|
||||||
|
uptime_seconds: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SystemStatus {
|
||||||
|
timestamp: number
|
||||||
|
uptime_seconds: number
|
||||||
|
active_websocket_connections: number
|
||||||
|
database_stats: {
|
||||||
|
total_sensors: number
|
||||||
|
active_sensors: number
|
||||||
|
total_readings: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataQuery {
|
||||||
|
sensor_ids?: string[]
|
||||||
|
rooms?: string[]
|
||||||
|
sensor_types?: SensorType[]
|
||||||
|
start_time?: number
|
||||||
|
end_time?: number
|
||||||
|
limit?: number
|
||||||
|
offset?: number
|
||||||
|
sort_by?: string
|
||||||
|
sort_order?: 'asc' | 'desc'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataResponse {
|
||||||
|
data: SensorReading[]
|
||||||
|
total_count: number
|
||||||
|
query: DataQuery
|
||||||
|
execution_time_ms: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SensorReading {
|
||||||
|
_id?: string
|
||||||
|
sensor_id: string
|
||||||
|
room?: string
|
||||||
|
sensor_type: string
|
||||||
|
timestamp: number
|
||||||
|
created_at?: string
|
||||||
|
energy?: {
|
||||||
|
value: number
|
||||||
|
unit: string
|
||||||
|
}
|
||||||
|
co2?: {
|
||||||
|
value: number
|
||||||
|
unit: string
|
||||||
|
}
|
||||||
|
temperature?: {
|
||||||
|
value: number
|
||||||
|
unit: string
|
||||||
|
}
|
||||||
|
humidity?: {
|
||||||
|
value: number
|
||||||
|
unit: string
|
||||||
|
}
|
||||||
|
metadata?: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SensorInfo {
|
||||||
|
sensor_id: string
|
||||||
|
sensor_type: SensorType
|
||||||
|
room?: string
|
||||||
|
status: SensorStatus
|
||||||
|
first_seen: number
|
||||||
|
last_seen: number
|
||||||
|
total_readings: number
|
||||||
|
latest_values?: Record<string, any>
|
||||||
|
metadata?: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RoomInfo {
|
||||||
|
room: string
|
||||||
|
sensor_count: number
|
||||||
|
sensor_types: SensorType[]
|
||||||
|
latest_metrics?: {
|
||||||
|
energy?: { current: number; unit: string }
|
||||||
|
co2?: { current: number; unit: string }
|
||||||
|
temperature?: { current: number; unit: string }
|
||||||
|
humidity?: { current: number; unit: string }
|
||||||
|
}
|
||||||
|
last_updated?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RoomData {
|
||||||
|
room: string
|
||||||
|
sensors: string[]
|
||||||
|
data: SensorReading[]
|
||||||
|
aggregated_metrics: {
|
||||||
|
energy?: { total: number; average: number; current: number; unit: string }
|
||||||
|
co2?: { average: number; max: number; current: number; unit: string }
|
||||||
|
temperature?: { average: number; min: number; max: number; current: number; unit: string }
|
||||||
|
}
|
||||||
|
execution_time_ms?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnalyticsSummary {
|
||||||
|
period_hours: number
|
||||||
|
total_energy_consumption: { value: number; unit: string }
|
||||||
|
average_power: { value: number; unit: string }
|
||||||
|
peak_power: { value: number; unit: string; timestamp: number }
|
||||||
|
sensor_count: number
|
||||||
|
room_count: number
|
||||||
|
co2_analysis?: {
|
||||||
|
average_ppm: number
|
||||||
|
max_ppm: number
|
||||||
|
rooms_above_threshold: string[]
|
||||||
|
}
|
||||||
|
top_consuming_sensors: Array<{
|
||||||
|
sensor_id: string
|
||||||
|
room: string
|
||||||
|
consumption: number
|
||||||
|
unit: string
|
||||||
|
}>
|
||||||
|
top_consuming_rooms: Array<{
|
||||||
|
room: string
|
||||||
|
consumption: number
|
||||||
|
unit: string
|
||||||
|
sensor_count: number
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EnergyTrends {
|
||||||
|
period_hours: number
|
||||||
|
hourly_consumption: Array<{
|
||||||
|
hour: string
|
||||||
|
consumption: number
|
||||||
|
unit: string
|
||||||
|
}>
|
||||||
|
daily_averages: Array<{
|
||||||
|
date: string
|
||||||
|
average_consumption: number
|
||||||
|
unit: string
|
||||||
|
}>
|
||||||
|
trend_analysis: {
|
||||||
|
trend: 'increasing' | 'decreasing' | 'stable'
|
||||||
|
percentage_change: number
|
||||||
|
prediction_next_24h: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RoomComparison {
|
||||||
|
period_hours: number
|
||||||
|
rooms: Array<{
|
||||||
|
room: string
|
||||||
|
total_consumption: number
|
||||||
|
average_consumption: number
|
||||||
|
peak_consumption: number
|
||||||
|
sensor_count: number
|
||||||
|
efficiency_rating: 'excellent' | 'good' | 'average' | 'poor'
|
||||||
|
unit: string
|
||||||
|
}>
|
||||||
|
insights: {
|
||||||
|
most_efficient: string
|
||||||
|
least_efficient: string
|
||||||
|
total_consumption: number
|
||||||
|
average_per_room: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SystemEvent {
|
||||||
|
_id: string
|
||||||
|
timestamp: number
|
||||||
|
event_type: string
|
||||||
|
severity: 'info' | 'warning' | 'error' | 'critical'
|
||||||
|
message: string
|
||||||
|
details?: Record<string, any>
|
||||||
|
sensor_id?: string
|
||||||
|
room?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SensorType {
|
||||||
|
ENERGY = 'energy',
|
||||||
|
CO2 = 'co2',
|
||||||
|
TEMPERATURE = 'temperature',
|
||||||
|
HUMIDITY = 'humidity',
|
||||||
|
HVAC = 'hvac',
|
||||||
|
LIGHTING = 'lighting',
|
||||||
|
SECURITY = 'security',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SensorStatus {
|
||||||
|
ONLINE = 'online',
|
||||||
|
OFFLINE = 'offline',
|
||||||
|
ERROR = 'error',
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP Client class
|
||||||
|
class ApiClient {
|
||||||
|
private baseUrl: string
|
||||||
|
|
||||||
|
constructor(baseUrl: string = API_BASE_URL) {
|
||||||
|
this.baseUrl = baseUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAuthHeaders(): Record<string, string> {
|
||||||
|
// Dynamically get auth headers to avoid circular imports
|
||||||
|
try {
|
||||||
|
const authStore = (window as any).__AUTH_STORE__
|
||||||
|
if (authStore && typeof authStore.getAuthHeader === 'function') {
|
||||||
|
return authStore.getAuthHeader()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Could not get auth headers:', error)
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
||||||
|
const url = `${this.baseUrl}${endpoint}`
|
||||||
|
|
||||||
|
const authHeaders = this.getAuthHeaders()
|
||||||
|
const config: RequestInit = {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...authHeaders,
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, config)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}))
|
||||||
|
throw new Error(`HTTP ${response.status}: ${errorData.detail || response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json()
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
throw new Error(`API request failed: ${error.message}`)
|
||||||
|
}
|
||||||
|
throw new Error('Unknown API error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async get<T>(endpoint: string, params?: Record<string, any>): Promise<T> {
|
||||||
|
const url = new URL(`${this.baseUrl}${endpoint}`)
|
||||||
|
|
||||||
|
if (params) {
|
||||||
|
Object.keys(params).forEach((key) => {
|
||||||
|
const value = params[key]
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach((v) => url.searchParams.append(key, String(v)))
|
||||||
|
} else {
|
||||||
|
url.searchParams.append(key, String(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const authHeaders = this.getAuthHeaders()
|
||||||
|
const response = await fetch(url.toString(), {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...authHeaders,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}))
|
||||||
|
throw new Error(`HTTP ${response.status}: ${errorData.detail || response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
async post<T>(endpoint: string, data?: any): Promise<T> {
|
||||||
|
return this.request<T>(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
body: data ? JSON.stringify(data) : undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async put<T>(endpoint: string, data?: any): Promise<T> {
|
||||||
|
return this.request<T>(endpoint, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: data ? JSON.stringify(data) : undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete<T>(endpoint: string): Promise<T> {
|
||||||
|
return this.request<T>(endpoint, {
|
||||||
|
method: 'DELETE',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create singleton instance
|
||||||
|
export const apiClient = new ApiClient()
|
||||||
|
|
||||||
|
// Health and status endpoints
|
||||||
|
export const healthApi = {
|
||||||
|
/**
|
||||||
|
* Get system health status
|
||||||
|
*/
|
||||||
|
async getHealth(): Promise<HealthCheck> {
|
||||||
|
return apiClient.get<HealthCheck>('/health')
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get detailed system status
|
||||||
|
*/
|
||||||
|
async getStatus(): Promise<SystemStatus> {
|
||||||
|
return apiClient.get<SystemStatus>('/api/v1/overview')
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default apiClient
|
||||||
47
src/services/authApi.ts
Normal file
47
src/services/authApi.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* Authentication API Service
|
||||||
|
* Handles JWT token generation and validation
|
||||||
|
*/
|
||||||
|
import { apiClient } from './api'
|
||||||
|
|
||||||
|
export interface TokenRequest {
|
||||||
|
name: string
|
||||||
|
list_of_resources: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TokenResponse {
|
||||||
|
token: string
|
||||||
|
expires_at: string
|
||||||
|
resources: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TokenValidation {
|
||||||
|
valid: boolean
|
||||||
|
expires_at?: string
|
||||||
|
resources?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const authApi = {
|
||||||
|
/**
|
||||||
|
* Generate a new JWT token for the dashboard
|
||||||
|
*/
|
||||||
|
async generateToken(request: TokenRequest): Promise<TokenResponse> {
|
||||||
|
return apiClient.post<TokenResponse>('/api/v1/tokens/generate', request)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate an existing token
|
||||||
|
*/
|
||||||
|
async validateToken(token: string): Promise<TokenValidation> {
|
||||||
|
return apiClient.post<TokenValidation>('/api/v1/tokens/validate', { token })
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revoke a token
|
||||||
|
*/
|
||||||
|
async revokeToken(token: string): Promise<{ message: string }> {
|
||||||
|
return apiClient.post<{ message: string }>('/api/v1/tokens/revoke', { token })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default authApi
|
||||||
30
src/services/index.ts
Normal file
30
src/services/index.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* API Services Index
|
||||||
|
* Central export point for all API services
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Export base API client and types
|
||||||
|
export * from './api'
|
||||||
|
|
||||||
|
// Export service modules
|
||||||
|
export { sensorsApi } from './sensorsApi'
|
||||||
|
export { roomsApi } from './roomsApi'
|
||||||
|
export { analyticsApi } from './analyticsApi'
|
||||||
|
export { healthApi } from './api'
|
||||||
|
export { authApi } from './authApi'
|
||||||
|
|
||||||
|
// Re-export commonly used types for convenience
|
||||||
|
export type {
|
||||||
|
SensorReading,
|
||||||
|
SensorInfo,
|
||||||
|
RoomInfo,
|
||||||
|
RoomData,
|
||||||
|
DataQuery,
|
||||||
|
DataResponse,
|
||||||
|
AnalyticsSummary,
|
||||||
|
EnergyTrends,
|
||||||
|
RoomComparison,
|
||||||
|
SystemEvent,
|
||||||
|
HealthCheck,
|
||||||
|
SystemStatus
|
||||||
|
} from './api'
|
||||||
201
src/stores/auth.ts
Normal file
201
src/stores/auth.ts
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { authApi, type TokenRequest, type TokenResponse } from '@/services/authApi'
|
||||||
|
|
||||||
|
const TOKEN_STORAGE_KEY = 'dashboard_auth_token'
|
||||||
|
const TOKEN_EXPIRY_KEY = 'dashboard_token_expiry'
|
||||||
|
const TOKEN_RESOURCES_KEY = 'dashboard_token_resources'
|
||||||
|
|
||||||
|
export const useAuthStore = defineStore('auth', () => {
|
||||||
|
// State
|
||||||
|
const token = ref<string | null>(null)
|
||||||
|
const tokenExpiry = ref<string | null>(null)
|
||||||
|
const tokenResources = ref<string[]>([])
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const error = ref<string | null>(null)
|
||||||
|
|
||||||
|
// Computed
|
||||||
|
const isAuthenticated = computed(() => {
|
||||||
|
if (!token.value || !tokenExpiry.value) return false
|
||||||
|
|
||||||
|
// Check if token is expired
|
||||||
|
const expiryTime = new Date(tokenExpiry.value).getTime()
|
||||||
|
const currentTime = new Date().getTime()
|
||||||
|
|
||||||
|
if (currentTime >= expiryTime) {
|
||||||
|
clearToken()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
const timeUntilExpiry = computed(() => {
|
||||||
|
if (!tokenExpiry.value) return null
|
||||||
|
|
||||||
|
const expiryTime = new Date(tokenExpiry.value).getTime()
|
||||||
|
const currentTime = new Date().getTime()
|
||||||
|
const diff = expiryTime - currentTime
|
||||||
|
|
||||||
|
if (diff <= 0) return null
|
||||||
|
|
||||||
|
const hours = Math.floor(diff / (1000 * 60 * 60))
|
||||||
|
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
|
||||||
|
|
||||||
|
return { hours, minutes, milliseconds: diff }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
async function generateToken(name: string = 'dashboard_user'): Promise<boolean> {
|
||||||
|
isLoading.value = true
|
||||||
|
error.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request: TokenRequest = {
|
||||||
|
name,
|
||||||
|
list_of_resources: [
|
||||||
|
'sensors',
|
||||||
|
'rooms',
|
||||||
|
'analytics',
|
||||||
|
'health',
|
||||||
|
'data',
|
||||||
|
'export',
|
||||||
|
'events'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await authApi.generateToken(request)
|
||||||
|
|
||||||
|
// Store token data
|
||||||
|
token.value = response.token
|
||||||
|
tokenExpiry.value = response.expires_at
|
||||||
|
tokenResources.value = response.resources || request.list_of_resources
|
||||||
|
|
||||||
|
// Persist to localStorage
|
||||||
|
localStorage.setItem(TOKEN_STORAGE_KEY, response.token)
|
||||||
|
localStorage.setItem(TOKEN_EXPIRY_KEY, response.expires_at)
|
||||||
|
localStorage.setItem(TOKEN_RESOURCES_KEY, JSON.stringify(tokenResources.value))
|
||||||
|
|
||||||
|
console.log('Authentication successful, token expires at:', response.expires_at)
|
||||||
|
return true
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Failed to generate token'
|
||||||
|
error.value = errorMessage
|
||||||
|
console.error('Authentication failed:', errorMessage)
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateToken(): Promise<boolean> {
|
||||||
|
if (!token.value) return false
|
||||||
|
|
||||||
|
try {
|
||||||
|
const validation = await authApi.validateToken(token.value)
|
||||||
|
|
||||||
|
if (!validation.valid) {
|
||||||
|
clearToken()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update expiry if provided
|
||||||
|
if (validation.expires_at) {
|
||||||
|
tokenExpiry.value = validation.expires_at
|
||||||
|
localStorage.setItem(TOKEN_EXPIRY_KEY, validation.expires_at)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Token validation failed:', err)
|
||||||
|
clearToken()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearToken() {
|
||||||
|
token.value = null
|
||||||
|
tokenExpiry.value = null
|
||||||
|
tokenResources.value = []
|
||||||
|
|
||||||
|
localStorage.removeItem(TOKEN_STORAGE_KEY)
|
||||||
|
localStorage.removeItem(TOKEN_EXPIRY_KEY)
|
||||||
|
localStorage.removeItem(TOKEN_RESOURCES_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadTokenFromStorage() {
|
||||||
|
const storedToken = localStorage.getItem(TOKEN_STORAGE_KEY)
|
||||||
|
const storedExpiry = localStorage.getItem(TOKEN_EXPIRY_KEY)
|
||||||
|
const storedResources = localStorage.getItem(TOKEN_RESOURCES_KEY)
|
||||||
|
|
||||||
|
if (storedToken && storedExpiry) {
|
||||||
|
// Check if token is still valid
|
||||||
|
const expiryTime = new Date(storedExpiry).getTime()
|
||||||
|
const currentTime = new Date().getTime()
|
||||||
|
|
||||||
|
if (currentTime < expiryTime) {
|
||||||
|
token.value = storedToken
|
||||||
|
tokenExpiry.value = storedExpiry
|
||||||
|
|
||||||
|
if (storedResources) {
|
||||||
|
try {
|
||||||
|
tokenResources.value = JSON.parse(storedResources)
|
||||||
|
} catch {
|
||||||
|
tokenResources.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Loaded valid token from storage')
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
// Token expired, clear it
|
||||||
|
clearToken()
|
||||||
|
console.log('Stored token was expired, cleared')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureAuthenticated(): Promise<boolean> {
|
||||||
|
// Try to load from storage first
|
||||||
|
if (loadTokenFromStorage() && isAuthenticated.value) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new token
|
||||||
|
return await generateToken()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get auth header for API requests
|
||||||
|
function getAuthHeader(): Record<string, string> {
|
||||||
|
if (token.value) {
|
||||||
|
return { Authorization: `Bearer ${token.value}` }
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize on store creation
|
||||||
|
loadTokenFromStorage()
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
token: computed(() => token.value),
|
||||||
|
tokenExpiry: computed(() => tokenExpiry.value),
|
||||||
|
tokenResources: computed(() => tokenResources.value),
|
||||||
|
isLoading: computed(() => isLoading.value),
|
||||||
|
error: computed(() => error.value),
|
||||||
|
|
||||||
|
// Computed
|
||||||
|
isAuthenticated,
|
||||||
|
timeUntilExpiry,
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
generateToken,
|
||||||
|
validateToken,
|
||||||
|
clearToken,
|
||||||
|
loadTokenFromStorage,
|
||||||
|
ensureAuthenticated,
|
||||||
|
getAuthHeader
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -43,18 +43,18 @@ const DEFAULT_SETTINGS: AppSettings = {
|
|||||||
autoRefresh: true,
|
autoRefresh: true,
|
||||||
refreshInterval: 5,
|
refreshInterval: 5,
|
||||||
dateFormat: 'relative',
|
dateFormat: 'relative',
|
||||||
timeFormat: '12h'
|
timeFormat: '12h',
|
||||||
},
|
},
|
||||||
notifications: {
|
notifications: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
sound: true,
|
sound: true,
|
||||||
desktop: false,
|
desktop: false,
|
||||||
email: false,
|
email: false,
|
||||||
criticalOnly: false
|
criticalOnly: false,
|
||||||
},
|
},
|
||||||
autoConnect: true,
|
autoConnect: true,
|
||||||
websocketUrl: 'ws://192.168.1.73:8000/ws',
|
websocketUrl: 'ws://localhost:8000/ws',
|
||||||
developerMode: false
|
developerMode: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useSettingsStore = defineStore('settings', () => {
|
export const useSettingsStore = defineStore('settings', () => {
|
||||||
@@ -103,11 +103,11 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
function updateSetting(path: string, value: any) {
|
function updateSetting(path: string, value: any) {
|
||||||
const keys = path.split('.')
|
const keys = path.split('.')
|
||||||
let current: any = settings
|
let current: any = settings
|
||||||
|
|
||||||
for (let i = 0; i < keys.length - 1; i++) {
|
for (let i = 0; i < keys.length - 1; i++) {
|
||||||
current = current[keys[i]]
|
current = current[keys[i]]
|
||||||
}
|
}
|
||||||
|
|
||||||
current[keys[keys.length - 1]] = value
|
current[keys[keys.length - 1]] = value
|
||||||
saveSettings()
|
saveSettings()
|
||||||
}
|
}
|
||||||
@@ -116,12 +116,12 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
function getSetting(path: string): any {
|
function getSetting(path: string): any {
|
||||||
const keys = path.split('.')
|
const keys = path.split('.')
|
||||||
let current: any = settings
|
let current: any = settings
|
||||||
|
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
current = current[key]
|
current = current[key]
|
||||||
if (current === undefined) break
|
if (current === undefined) break
|
||||||
}
|
}
|
||||||
|
|
||||||
return current
|
return current
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
// Theme helpers
|
// Theme helpers
|
||||||
function applyTheme() {
|
function applyTheme() {
|
||||||
const root = document.documentElement
|
const root = document.documentElement
|
||||||
|
|
||||||
if (settings.theme === 'dark') {
|
if (settings.theme === 'dark') {
|
||||||
root.classList.add('dark')
|
root.classList.add('dark')
|
||||||
} else if (settings.theme === 'light') {
|
} else if (settings.theme === 'light') {
|
||||||
@@ -206,10 +206,14 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
|
|
||||||
// Auto-save on changes (debounced)
|
// Auto-save on changes (debounced)
|
||||||
let saveTimeout: number | undefined
|
let saveTimeout: number | undefined
|
||||||
watch(settings, () => {
|
watch(
|
||||||
if (saveTimeout) clearTimeout(saveTimeout)
|
settings,
|
||||||
saveTimeout = window.setTimeout(saveSettings, 500)
|
() => {
|
||||||
}, { deep: true })
|
if (saveTimeout) clearTimeout(saveTimeout)
|
||||||
|
saveTimeout = window.setTimeout(saveSettings, 500)
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get available languages
|
// Get available languages
|
||||||
@@ -218,7 +222,7 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
{ code: 'en', name: 'English', nativeName: 'English' },
|
{ code: 'en', name: 'English', nativeName: 'English' },
|
||||||
{ code: 'es', name: 'Spanish', nativeName: 'Español' },
|
{ code: 'es', name: 'Spanish', nativeName: 'Español' },
|
||||||
{ code: 'fr', name: 'French', nativeName: 'Français' },
|
{ code: 'fr', name: 'French', nativeName: 'Français' },
|
||||||
{ code: 'de', name: 'German', nativeName: 'Deutsch' }
|
{ code: 'de', name: 'German', nativeName: 'Deutsch' },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,31 +231,31 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
return [
|
return [
|
||||||
{ value: 'system', label: 'System Default', icon: '🔄' },
|
{ value: 'system', label: 'System Default', icon: '🔄' },
|
||||||
{ value: 'light', label: 'Light Mode', icon: '☀️' },
|
{ value: 'light', label: 'Light Mode', icon: '☀️' },
|
||||||
{ value: 'dark', label: 'Dark Mode', icon: '🌙' }
|
{ value: 'dark', label: 'Dark Mode', icon: '🌙' },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get navigation mode options
|
// Get navigation mode options
|
||||||
function getNavigationModeOptions() {
|
function getNavigationModeOptions() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
value: 'hover',
|
value: 'hover',
|
||||||
label: 'Show on Hover',
|
label: 'Show on Hover',
|
||||||
description: 'Navigation appears when hovering near bottom (desktop only)',
|
description: 'Navigation appears when hovering near bottom (desktop only)',
|
||||||
icon: '👆'
|
icon: '👆',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'always',
|
value: 'always',
|
||||||
label: 'Always Visible',
|
label: 'Always Visible',
|
||||||
description: 'Navigation is permanently visible',
|
description: 'Navigation is permanently visible',
|
||||||
icon: '👁️'
|
icon: '👁️',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'hidden',
|
value: 'hidden',
|
||||||
label: 'Hidden',
|
label: 'Hidden',
|
||||||
description: 'Navigation is completely hidden',
|
description: 'Navigation is completely hidden',
|
||||||
icon: '🫥'
|
icon: '🫥',
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,7 +264,7 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
settings,
|
settings,
|
||||||
isLoading,
|
isLoading,
|
||||||
lastSaved,
|
lastSaved,
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
loadSettings,
|
loadSettings,
|
||||||
saveSettings,
|
saveSettings,
|
||||||
@@ -273,10 +277,10 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
isValidWebSocketUrl,
|
isValidWebSocketUrl,
|
||||||
requestNotificationPermission,
|
requestNotificationPermission,
|
||||||
initialize,
|
initialize,
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
getAvailableLanguages,
|
getAvailableLanguages,
|
||||||
getThemeOptions,
|
getThemeOptions,
|
||||||
getNavigationModeOptions
|
getNavigationModeOptions,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,11 +4,13 @@
|
|||||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-2xl font-bold text-gray-900">AI Optimization</h1>
|
<h1 class="text-2xl font-bold text-gray-900">AI Optimization</h1>
|
||||||
<p class="text-gray-600">Leverage artificial intelligence to optimize energy consumption and building operations</p>
|
<p class="text-gray-600">
|
||||||
|
Leverage artificial intelligence to optimize energy consumption and building operations
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4 sm:mt-0">
|
<div class="mt-4 sm:mt-0">
|
||||||
<div class="flex items-center gap-2 text-sm text-gray-600">
|
<div class="flex items-center gap-2 text-sm text-gray-600">
|
||||||
<div
|
<div
|
||||||
class="w-3 h-3 rounded-full"
|
class="w-3 h-3 rounded-full"
|
||||||
:class="energyStore.isConnected ? 'bg-green-500' : 'bg-red-500'"
|
:class="energyStore.isConnected ? 'bg-green-500' : 'bg-red-500'"
|
||||||
></div>
|
></div>
|
||||||
@@ -93,35 +95,61 @@
|
|||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-sm text-gray-600">Predictive Temperature Control</span>
|
<span class="text-sm text-gray-600">Predictive Temperature Control</span>
|
||||||
<button
|
<button
|
||||||
@click="toggleOptimization('hvac', 'predictive_temp')"
|
@click="toggleOptimization('hvac', 'predictive_temp')"
|
||||||
class="relative inline-flex h-5 w-9 items-center rounded-full transition-colors"
|
class="relative inline-flex h-5 w-9 items-center rounded-full transition-colors"
|
||||||
:class="isOptimizationEnabled('hvac', 'predictive_temp') ? 'bg-blue-600' : 'bg-gray-200'"
|
:class="
|
||||||
|
isOptimizationEnabled('hvac', 'predictive_temp') ? 'bg-blue-600' : 'bg-gray-200'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<span class="inline-block h-3 w-3 transform rounded-full bg-white transition-transform"
|
<span
|
||||||
:class="isOptimizationEnabled('hvac', 'predictive_temp') ? 'translate-x-5' : 'translate-x-1'"></span>
|
class="inline-block h-3 w-3 transform rounded-full bg-white transition-transform"
|
||||||
|
:class="
|
||||||
|
isOptimizationEnabled('hvac', 'predictive_temp')
|
||||||
|
? 'translate-x-5'
|
||||||
|
: 'translate-x-1'
|
||||||
|
"
|
||||||
|
></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-sm text-gray-600">Occupancy-based Scheduling</span>
|
<span class="text-sm text-gray-600">Occupancy-based Scheduling</span>
|
||||||
<button
|
<button
|
||||||
@click="toggleOptimization('hvac', 'occupancy_schedule')"
|
@click="toggleOptimization('hvac', 'occupancy_schedule')"
|
||||||
class="relative inline-flex h-5 w-9 items-center rounded-full transition-colors"
|
class="relative inline-flex h-5 w-9 items-center rounded-full transition-colors"
|
||||||
:class="isOptimizationEnabled('hvac', 'occupancy_schedule') ? 'bg-blue-600' : 'bg-gray-200'"
|
:class="
|
||||||
|
isOptimizationEnabled('hvac', 'occupancy_schedule')
|
||||||
|
? 'bg-blue-600'
|
||||||
|
: 'bg-gray-200'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<span class="inline-block h-3 w-3 transform rounded-full bg-white transition-transform"
|
<span
|
||||||
:class="isOptimizationEnabled('hvac', 'occupancy_schedule') ? 'translate-x-5' : 'translate-x-1'"></span>
|
class="inline-block h-3 w-3 transform rounded-full bg-white transition-transform"
|
||||||
|
:class="
|
||||||
|
isOptimizationEnabled('hvac', 'occupancy_schedule')
|
||||||
|
? 'translate-x-5'
|
||||||
|
: 'translate-x-1'
|
||||||
|
"
|
||||||
|
></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-sm text-gray-600">Weather-based Adjustments</span>
|
<span class="text-sm text-gray-600">Weather-based Adjustments</span>
|
||||||
<button
|
<button
|
||||||
@click="toggleOptimization('hvac', 'weather_adjust')"
|
@click="toggleOptimization('hvac', 'weather_adjust')"
|
||||||
class="relative inline-flex h-5 w-9 items-center rounded-full transition-colors"
|
class="relative inline-flex h-5 w-9 items-center rounded-full transition-colors"
|
||||||
:class="isOptimizationEnabled('hvac', 'weather_adjust') ? 'bg-blue-600' : 'bg-gray-200'"
|
:class="
|
||||||
|
isOptimizationEnabled('hvac', 'weather_adjust') ? 'bg-blue-600' : 'bg-gray-200'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<span class="inline-block h-3 w-3 transform rounded-full bg-white transition-transform"
|
<span
|
||||||
:class="isOptimizationEnabled('hvac', 'weather_adjust') ? 'translate-x-5' : 'translate-x-1'"></span>
|
class="inline-block h-3 w-3 transform rounded-full bg-white transition-transform"
|
||||||
|
:class="
|
||||||
|
isOptimizationEnabled('hvac', 'weather_adjust')
|
||||||
|
? 'translate-x-5'
|
||||||
|
: 'translate-x-1'
|
||||||
|
"
|
||||||
|
></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -163,35 +191,65 @@
|
|||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-sm text-gray-600">Daylight Harvesting</span>
|
<span class="text-sm text-gray-600">Daylight Harvesting</span>
|
||||||
<button
|
<button
|
||||||
@click="toggleOptimization('lighting', 'daylight_harvest')"
|
@click="toggleOptimization('lighting', 'daylight_harvest')"
|
||||||
class="relative inline-flex h-5 w-9 items-center rounded-full transition-colors"
|
class="relative inline-flex h-5 w-9 items-center rounded-full transition-colors"
|
||||||
:class="isOptimizationEnabled('lighting', 'daylight_harvest') ? 'bg-blue-600' : 'bg-gray-200'"
|
:class="
|
||||||
|
isOptimizationEnabled('lighting', 'daylight_harvest')
|
||||||
|
? 'bg-blue-600'
|
||||||
|
: 'bg-gray-200'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<span class="inline-block h-3 w-3 transform rounded-full bg-white transition-transform"
|
<span
|
||||||
:class="isOptimizationEnabled('lighting', 'daylight_harvest') ? 'translate-x-5' : 'translate-x-1'"></span>
|
class="inline-block h-3 w-3 transform rounded-full bg-white transition-transform"
|
||||||
|
:class="
|
||||||
|
isOptimizationEnabled('lighting', 'daylight_harvest')
|
||||||
|
? 'translate-x-5'
|
||||||
|
: 'translate-x-1'
|
||||||
|
"
|
||||||
|
></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-sm text-gray-600">Motion-based Control</span>
|
<span class="text-sm text-gray-600">Motion-based Control</span>
|
||||||
<button
|
<button
|
||||||
@click="toggleOptimization('lighting', 'motion_control')"
|
@click="toggleOptimization('lighting', 'motion_control')"
|
||||||
class="relative inline-flex h-5 w-9 items-center rounded-full transition-colors"
|
class="relative inline-flex h-5 w-9 items-center rounded-full transition-colors"
|
||||||
:class="isOptimizationEnabled('lighting', 'motion_control') ? 'bg-blue-600' : 'bg-gray-200'"
|
:class="
|
||||||
|
isOptimizationEnabled('lighting', 'motion_control')
|
||||||
|
? 'bg-blue-600'
|
||||||
|
: 'bg-gray-200'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<span class="inline-block h-3 w-3 transform rounded-full bg-white transition-transform"
|
<span
|
||||||
:class="isOptimizationEnabled('lighting', 'motion_control') ? 'translate-x-5' : 'translate-x-1'"></span>
|
class="inline-block h-3 w-3 transform rounded-full bg-white transition-transform"
|
||||||
|
:class="
|
||||||
|
isOptimizationEnabled('lighting', 'motion_control')
|
||||||
|
? 'translate-x-5'
|
||||||
|
: 'translate-x-1'
|
||||||
|
"
|
||||||
|
></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-sm text-gray-600">Circadian Rhythm Sync</span>
|
<span class="text-sm text-gray-600">Circadian Rhythm Sync</span>
|
||||||
<button
|
<button
|
||||||
@click="toggleOptimization('lighting', 'circadian_sync')"
|
@click="toggleOptimization('lighting', 'circadian_sync')"
|
||||||
class="relative inline-flex h-5 w-9 items-center rounded-full transition-colors"
|
class="relative inline-flex h-5 w-9 items-center rounded-full transition-colors"
|
||||||
:class="isOptimizationEnabled('lighting', 'circadian_sync') ? 'bg-blue-600' : 'bg-gray-200'"
|
:class="
|
||||||
|
isOptimizationEnabled('lighting', 'circadian_sync')
|
||||||
|
? 'bg-blue-600'
|
||||||
|
: 'bg-gray-200'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<span class="inline-block h-3 w-3 transform rounded-full bg-white transition-transform"
|
<span
|
||||||
:class="isOptimizationEnabled('lighting', 'circadian_sync') ? 'translate-x-5' : 'translate-x-1'"></span>
|
class="inline-block h-3 w-3 transform rounded-full bg-white transition-transform"
|
||||||
|
:class="
|
||||||
|
isOptimizationEnabled('lighting', 'circadian_sync')
|
||||||
|
? 'translate-x-5'
|
||||||
|
: 'translate-x-1'
|
||||||
|
"
|
||||||
|
></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -223,7 +281,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-xs px-2 py-1 bg-yellow-100 text-yellow-700 rounded-full font-medium">
|
<span
|
||||||
|
class="text-xs px-2 py-1 bg-yellow-100 text-yellow-700 rounded-full font-medium"
|
||||||
|
>
|
||||||
Partial
|
Partial
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -233,35 +293,65 @@
|
|||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-sm text-gray-600">Adaptive Access Control</span>
|
<span class="text-sm text-gray-600">Adaptive Access Control</span>
|
||||||
<button
|
<button
|
||||||
@click="toggleOptimization('security', 'adaptive_access')"
|
@click="toggleOptimization('security', 'adaptive_access')"
|
||||||
class="relative inline-flex h-5 w-9 items-center rounded-full transition-colors"
|
class="relative inline-flex h-5 w-9 items-center rounded-full transition-colors"
|
||||||
:class="isOptimizationEnabled('security', 'adaptive_access') ? 'bg-blue-600' : 'bg-gray-200'"
|
:class="
|
||||||
|
isOptimizationEnabled('security', 'adaptive_access')
|
||||||
|
? 'bg-blue-600'
|
||||||
|
: 'bg-gray-200'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<span class="inline-block h-3 w-3 transform rounded-full bg-white transition-transform"
|
<span
|
||||||
:class="isOptimizationEnabled('security', 'adaptive_access') ? 'translate-x-5' : 'translate-x-1'"></span>
|
class="inline-block h-3 w-3 transform rounded-full bg-white transition-transform"
|
||||||
|
:class="
|
||||||
|
isOptimizationEnabled('security', 'adaptive_access')
|
||||||
|
? 'translate-x-5'
|
||||||
|
: 'translate-x-1'
|
||||||
|
"
|
||||||
|
></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-sm text-gray-600">Anomaly Detection</span>
|
<span class="text-sm text-gray-600">Anomaly Detection</span>
|
||||||
<button
|
<button
|
||||||
@click="toggleOptimization('security', 'anomaly_detection')"
|
@click="toggleOptimization('security', 'anomaly_detection')"
|
||||||
class="relative inline-flex h-5 w-9 items-center rounded-full transition-colors"
|
class="relative inline-flex h-5 w-9 items-center rounded-full transition-colors"
|
||||||
:class="isOptimizationEnabled('security', 'anomaly_detection') ? 'bg-blue-600' : 'bg-gray-200'"
|
:class="
|
||||||
|
isOptimizationEnabled('security', 'anomaly_detection')
|
||||||
|
? 'bg-blue-600'
|
||||||
|
: 'bg-gray-200'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<span class="inline-block h-3 w-3 transform rounded-full bg-white transition-transform"
|
<span
|
||||||
:class="isOptimizationEnabled('security', 'anomaly_detection') ? 'translate-x-5' : 'translate-x-1'"></span>
|
class="inline-block h-3 w-3 transform rounded-full bg-white transition-transform"
|
||||||
|
:class="
|
||||||
|
isOptimizationEnabled('security', 'anomaly_detection')
|
||||||
|
? 'translate-x-5'
|
||||||
|
: 'translate-x-1'
|
||||||
|
"
|
||||||
|
></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-sm text-gray-600">Predictive Maintenance</span>
|
<span class="text-sm text-gray-600">Predictive Maintenance</span>
|
||||||
<button
|
<button
|
||||||
@click="toggleOptimization('security', 'predictive_maintenance')"
|
@click="toggleOptimization('security', 'predictive_maintenance')"
|
||||||
class="relative inline-flex h-5 w-9 items-center rounded-full transition-colors"
|
class="relative inline-flex h-5 w-9 items-center rounded-full transition-colors"
|
||||||
:class="isOptimizationEnabled('security', 'predictive_maintenance') ? 'bg-blue-600' : 'bg-gray-200'"
|
:class="
|
||||||
|
isOptimizationEnabled('security', 'predictive_maintenance')
|
||||||
|
? 'bg-blue-600'
|
||||||
|
: 'bg-gray-200'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<span class="inline-block h-3 w-3 transform rounded-full bg-white transition-transform"
|
<span
|
||||||
:class="isOptimizationEnabled('security', 'predictive_maintenance') ? 'translate-x-5' : 'translate-x-1'"></span>
|
class="inline-block h-3 w-3 transform rounded-full bg-white transition-transform"
|
||||||
|
:class="
|
||||||
|
isOptimizationEnabled('security', 'predictive_maintenance')
|
||||||
|
? 'translate-x-5'
|
||||||
|
: 'translate-x-1'
|
||||||
|
"
|
||||||
|
></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -303,35 +393,65 @@
|
|||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-sm text-gray-600">CO2-based Ventilation</span>
|
<span class="text-sm text-gray-600">CO2-based Ventilation</span>
|
||||||
<button
|
<button
|
||||||
@click="toggleOptimization('air_quality', 'co2_ventilation')"
|
@click="toggleOptimization('air_quality', 'co2_ventilation')"
|
||||||
class="relative inline-flex h-5 w-9 items-center rounded-full transition-colors"
|
class="relative inline-flex h-5 w-9 items-center rounded-full transition-colors"
|
||||||
:class="isOptimizationEnabled('air_quality', 'co2_ventilation') ? 'bg-blue-600' : 'bg-gray-200'"
|
:class="
|
||||||
|
isOptimizationEnabled('air_quality', 'co2_ventilation')
|
||||||
|
? 'bg-blue-600'
|
||||||
|
: 'bg-gray-200'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<span class="inline-block h-3 w-3 transform rounded-full bg-white transition-transform"
|
<span
|
||||||
:class="isOptimizationEnabled('air_quality', 'co2_ventilation') ? 'translate-x-5' : 'translate-x-1'"></span>
|
class="inline-block h-3 w-3 transform rounded-full bg-white transition-transform"
|
||||||
|
:class="
|
||||||
|
isOptimizationEnabled('air_quality', 'co2_ventilation')
|
||||||
|
? 'translate-x-5'
|
||||||
|
: 'translate-x-1'
|
||||||
|
"
|
||||||
|
></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-sm text-gray-600">Smart Air Filtration</span>
|
<span class="text-sm text-gray-600">Smart Air Filtration</span>
|
||||||
<button
|
<button
|
||||||
@click="toggleOptimization('air_quality', 'smart_filtration')"
|
@click="toggleOptimization('air_quality', 'smart_filtration')"
|
||||||
class="relative inline-flex h-5 w-9 items-center rounded-full transition-colors"
|
class="relative inline-flex h-5 w-9 items-center rounded-full transition-colors"
|
||||||
:class="isOptimizationEnabled('air_quality', 'smart_filtration') ? 'bg-blue-600' : 'bg-gray-200'"
|
:class="
|
||||||
|
isOptimizationEnabled('air_quality', 'smart_filtration')
|
||||||
|
? 'bg-blue-600'
|
||||||
|
: 'bg-gray-200'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<span class="inline-block h-3 w-3 transform rounded-full bg-white transition-transform"
|
<span
|
||||||
:class="isOptimizationEnabled('air_quality', 'smart_filtration') ? 'translate-x-5' : 'translate-x-1'"></span>
|
class="inline-block h-3 w-3 transform rounded-full bg-white transition-transform"
|
||||||
|
:class="
|
||||||
|
isOptimizationEnabled('air_quality', 'smart_filtration')
|
||||||
|
? 'translate-x-5'
|
||||||
|
: 'translate-x-1'
|
||||||
|
"
|
||||||
|
></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-sm text-gray-600">Humidity Control</span>
|
<span class="text-sm text-gray-600">Humidity Control</span>
|
||||||
<button
|
<button
|
||||||
@click="toggleOptimization('air_quality', 'humidity_control')"
|
@click="toggleOptimization('air_quality', 'humidity_control')"
|
||||||
class="relative inline-flex h-5 w-9 items-center rounded-full transition-colors"
|
class="relative inline-flex h-5 w-9 items-center rounded-full transition-colors"
|
||||||
:class="isOptimizationEnabled('air_quality', 'humidity_control') ? 'bg-blue-600' : 'bg-gray-200'"
|
:class="
|
||||||
|
isOptimizationEnabled('air_quality', 'humidity_control')
|
||||||
|
? 'bg-blue-600'
|
||||||
|
: 'bg-gray-200'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<span class="inline-block h-3 w-3 transform rounded-full bg-white transition-transform"
|
<span
|
||||||
:class="isOptimizationEnabled('air_quality', 'humidity_control') ? 'translate-x-5' : 'translate-x-1'"></span>
|
class="inline-block h-3 w-3 transform rounded-full bg-white transition-transform"
|
||||||
|
:class="
|
||||||
|
isOptimizationEnabled('air_quality', 'humidity_control')
|
||||||
|
? 'translate-x-5'
|
||||||
|
: 'translate-x-1'
|
||||||
|
"
|
||||||
|
></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -370,7 +490,7 @@
|
|||||||
<div class="text-xs text-gray-500">{{ optimization.description }}</div>
|
<div class="text-xs text-gray-500">{{ optimization.description }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@click="pauseOptimization(optimization.id)"
|
@click="pauseOptimization(optimization.id)"
|
||||||
class="text-xs px-2 py-1 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded font-medium transition-colors"
|
class="text-xs px-2 py-1 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded font-medium transition-colors"
|
||||||
>
|
>
|
||||||
@@ -379,12 +499,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between text-xs text-gray-600">
|
<div class="flex items-center justify-between text-xs text-gray-600">
|
||||||
<span>Started: {{ optimization.startTime }}</span>
|
<span>Started: {{ optimization.startTime }}</span>
|
||||||
<span class="font-medium" :class="optimization.impactColor">{{ optimization.impact }}</span>
|
<span class="font-medium" :class="optimization.impactColor">{{
|
||||||
|
optimization.impact
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-4 border-t border-gray-100">
|
<div class="p-4 border-t border-gray-100">
|
||||||
<button
|
<button
|
||||||
@click="showNewOptimizationModal = true"
|
@click="showNewOptimizationModal = true"
|
||||||
class="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium transition-colors"
|
class="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium transition-colors"
|
||||||
>
|
>
|
||||||
@@ -415,13 +537,11 @@
|
|||||||
{{ schedule.frequency }}
|
{{ schedule.frequency }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-gray-600">
|
<div class="text-xs text-gray-600">Next run: {{ schedule.nextRun }}</div>
|
||||||
Next run: {{ schedule.nextRun }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-4 border-t border-gray-100">
|
<div class="p-4 border-t border-gray-100">
|
||||||
<button
|
<button
|
||||||
@click="showScheduleModal = true"
|
@click="showScheduleModal = true"
|
||||||
class="w-full px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg text-sm font-medium transition-colors"
|
class="w-full px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg text-sm font-medium transition-colors"
|
||||||
>
|
>
|
||||||
@@ -432,32 +552,41 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- New Optimization Modal Placeholder -->
|
<!-- New Optimization Modal Placeholder -->
|
||||||
<div v-if="showNewOptimizationModal" class="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
|
<div
|
||||||
|
v-if="showNewOptimizationModal"
|
||||||
|
class="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50"
|
||||||
|
>
|
||||||
<div class="bg-white rounded-xl max-w-md w-full p-6">
|
<div class="bg-white rounded-xl max-w-md w-full p-6">
|
||||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">New AI Optimization</h3>
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">New AI Optimization</h3>
|
||||||
<p class="text-gray-600 mb-4">Select an optimization type to configure:</p>
|
<p class="text-gray-600 mb-4">Select an optimization type to configure:</p>
|
||||||
<div class="space-y-2 mb-6">
|
<div class="space-y-2 mb-6">
|
||||||
<button class="w-full p-3 text-left border border-gray-200 rounded-lg hover:border-blue-300 transition-colors">
|
<button
|
||||||
|
class="w-full p-3 text-left border border-gray-200 rounded-lg hover:border-blue-300 transition-colors"
|
||||||
|
>
|
||||||
<div class="font-medium text-gray-900">Custom HVAC Schedule</div>
|
<div class="font-medium text-gray-900">Custom HVAC Schedule</div>
|
||||||
<div class="text-sm text-gray-600">Create room-specific temperature schedules</div>
|
<div class="text-sm text-gray-600">Create room-specific temperature schedules</div>
|
||||||
</button>
|
</button>
|
||||||
<button class="w-full p-3 text-left border border-gray-200 rounded-lg hover:border-blue-300 transition-colors">
|
<button
|
||||||
|
class="w-full p-3 text-left border border-gray-200 rounded-lg hover:border-blue-300 transition-colors"
|
||||||
|
>
|
||||||
<div class="font-medium text-gray-900">Energy Load Balancing</div>
|
<div class="font-medium text-gray-900">Energy Load Balancing</div>
|
||||||
<div class="text-sm text-gray-600">Distribute energy usage across peak times</div>
|
<div class="text-sm text-gray-600">Distribute energy usage across peak times</div>
|
||||||
</button>
|
</button>
|
||||||
<button class="w-full p-3 text-left border border-gray-200 rounded-lg hover:border-blue-300 transition-colors">
|
<button
|
||||||
|
class="w-full p-3 text-left border border-gray-200 rounded-lg hover:border-blue-300 transition-colors"
|
||||||
|
>
|
||||||
<div class="font-medium text-gray-900">Predictive Maintenance</div>
|
<div class="font-medium text-gray-900">Predictive Maintenance</div>
|
||||||
<div class="text-sm text-gray-600">AI-driven equipment monitoring</div>
|
<div class="text-sm text-gray-600">AI-driven equipment monitoring</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
<button
|
<button
|
||||||
@click="showNewOptimizationModal = false"
|
@click="showNewOptimizationModal = false"
|
||||||
class="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg text-sm font-medium hover:bg-gray-50 transition-colors"
|
class="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg text-sm font-medium hover:bg-gray-50 transition-colors"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors"
|
class="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors"
|
||||||
>
|
>
|
||||||
Continue
|
Continue
|
||||||
@@ -483,23 +612,23 @@ const optimizations = ref({
|
|||||||
hvac: {
|
hvac: {
|
||||||
predictive_temp: true,
|
predictive_temp: true,
|
||||||
occupancy_schedule: true,
|
occupancy_schedule: true,
|
||||||
weather_adjust: false
|
weather_adjust: false,
|
||||||
},
|
},
|
||||||
lighting: {
|
lighting: {
|
||||||
daylight_harvest: true,
|
daylight_harvest: true,
|
||||||
motion_control: true,
|
motion_control: true,
|
||||||
circadian_sync: false
|
circadian_sync: false,
|
||||||
},
|
},
|
||||||
security: {
|
security: {
|
||||||
adaptive_access: false,
|
adaptive_access: false,
|
||||||
anomaly_detection: true,
|
anomaly_detection: true,
|
||||||
predictive_maintenance: false
|
predictive_maintenance: false,
|
||||||
},
|
},
|
||||||
air_quality: {
|
air_quality: {
|
||||||
co2_ventilation: true,
|
co2_ventilation: true,
|
||||||
smart_filtration: true,
|
smart_filtration: true,
|
||||||
humidity_control: false
|
humidity_control: false,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Mock data for active optimizations
|
// Mock data for active optimizations
|
||||||
@@ -512,7 +641,7 @@ const activeOptimizations = ref([
|
|||||||
statusColor: 'bg-green-100',
|
statusColor: 'bg-green-100',
|
||||||
startTime: '2 hours ago',
|
startTime: '2 hours ago',
|
||||||
impact: '-15.2% energy',
|
impact: '-15.2% energy',
|
||||||
impactColor: 'text-green-600'
|
impactColor: 'text-green-600',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
@@ -522,7 +651,7 @@ const activeOptimizations = ref([
|
|||||||
statusColor: 'bg-amber-100',
|
statusColor: 'bg-amber-100',
|
||||||
startTime: '45 minutes ago',
|
startTime: '45 minutes ago',
|
||||||
impact: '-8.7% energy',
|
impact: '-8.7% energy',
|
||||||
impactColor: 'text-green-600'
|
impactColor: 'text-green-600',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
@@ -532,8 +661,8 @@ const activeOptimizations = ref([
|
|||||||
statusColor: 'bg-blue-100',
|
statusColor: 'bg-blue-100',
|
||||||
startTime: '1 hour ago',
|
startTime: '1 hour ago',
|
||||||
impact: '+2.1% comfort',
|
impact: '+2.1% comfort',
|
||||||
impactColor: 'text-blue-600'
|
impactColor: 'text-blue-600',
|
||||||
}
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
// Mock data for upcoming optimizations
|
// Mock data for upcoming optimizations
|
||||||
@@ -544,7 +673,7 @@ const upcomingOptimizations = ref([
|
|||||||
icon: '⚡',
|
icon: '⚡',
|
||||||
rooms: ['All Zones'],
|
rooms: ['All Zones'],
|
||||||
frequency: 'Daily',
|
frequency: 'Daily',
|
||||||
nextRun: 'Today, 2:00 PM'
|
nextRun: 'Today, 2:00 PM',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
@@ -552,7 +681,7 @@ const upcomingOptimizations = ref([
|
|||||||
icon: '🌙',
|
icon: '🌙',
|
||||||
rooms: ['Office Floor 1', 'Office Floor 2'],
|
rooms: ['Office Floor 1', 'Office Floor 2'],
|
||||||
frequency: 'Daily',
|
frequency: 'Daily',
|
||||||
nextRun: 'Today, 6:00 PM'
|
nextRun: 'Today, 6:00 PM',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
@@ -560,8 +689,8 @@ const upcomingOptimizations = ref([
|
|||||||
icon: '🏢',
|
icon: '🏢',
|
||||||
rooms: ['Conference Rooms', 'Meeting Rooms'],
|
rooms: ['Conference Rooms', 'Meeting Rooms'],
|
||||||
frequency: 'Weekly',
|
frequency: 'Weekly',
|
||||||
nextRun: 'Saturday, 8:00 AM'
|
nextRun: 'Saturday, 8:00 AM',
|
||||||
}
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
// Computed properties
|
// Computed properties
|
||||||
@@ -582,15 +711,15 @@ const toggleOptimization = (category: string, optimization: string) => {
|
|||||||
const current = isOptimizationEnabled(category, optimization)
|
const current = isOptimizationEnabled(category, optimization)
|
||||||
const categorySettings = optimizations.value[category as keyof typeof optimizations.value]
|
const categorySettings = optimizations.value[category as keyof typeof optimizations.value]
|
||||||
if (categorySettings) {
|
if (categorySettings) {
|
||||||
(categorySettings as any)[optimization] = !current
|
;(categorySettings as any)[optimization] = !current
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simulate API call
|
// Simulate API call
|
||||||
console.log(`${current ? 'Disabled' : 'Enabled'} ${category}.${optimization}`)
|
console.log(`${current ? 'Disabled' : 'Enabled'} ${category}.${optimization}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const pauseOptimization = (id: number) => {
|
const pauseOptimization = (id: number) => {
|
||||||
const optimization = activeOptimizations.value.find(opt => opt.id === id)
|
const optimization = activeOptimizations.value.find((opt) => opt.id === id)
|
||||||
if (optimization) {
|
if (optimization) {
|
||||||
console.log(`Pausing optimization: ${optimization.name}`)
|
console.log(`Pausing optimization: ${optimization.name}`)
|
||||||
// In a real app, this would make an API call
|
// In a real app, this would make an API call
|
||||||
@@ -600,7 +729,7 @@ const pauseOptimization = (id: number) => {
|
|||||||
// Initialize connection
|
// Initialize connection
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!energyStore.isConnected) {
|
if (!energyStore.isConnected) {
|
||||||
energyStore.connect('ws://192.168.1.73:8000/ws')
|
energyStore.connect('ws://localhost:8000/ws')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
121
test-websocket.html
Normal file
121
test-websocket.html
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>WebSocket Test</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>WebSocket Connection Test</h1>
|
||||||
|
<div id="status">Disconnected</div>
|
||||||
|
<div id="messages"></div>
|
||||||
|
<button onclick="connect()">Connect</button>
|
||||||
|
<button onclick="disconnect()">Disconnect</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let socket = null;
|
||||||
|
let isConnected = false;
|
||||||
|
|
||||||
|
function updateStatus(status) {
|
||||||
|
document.getElementById('status').textContent = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMessage(message) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = new Date().toLocaleTimeString() + ': ' + message;
|
||||||
|
document.getElementById('messages').appendChild(div);
|
||||||
|
}
|
||||||
|
|
||||||
|
function connect() {
|
||||||
|
if (isConnected) {
|
||||||
|
addMessage('Already connected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close any existing connection
|
||||||
|
if (socket) {
|
||||||
|
socket.close();
|
||||||
|
socket = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
addMessage('Connecting to ws://localhost:8000/ws...');
|
||||||
|
socket = new WebSocket('ws://localhost:8000/ws');
|
||||||
|
|
||||||
|
socket.onopen = () => {
|
||||||
|
addMessage('Connected to API Gateway');
|
||||||
|
updateStatus('Connected');
|
||||||
|
isConnected = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
addMessage('Received: ' + JSON.stringify(data));
|
||||||
|
|
||||||
|
// Handle proxy info
|
||||||
|
if (data.type === 'proxy_info') {
|
||||||
|
addMessage('Received proxy info, reconnecting to sensor service...');
|
||||||
|
socket.close();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
addMessage('Connecting to ws://localhost:8007/ws...');
|
||||||
|
socket = new WebSocket('ws://localhost:8007/ws');
|
||||||
|
|
||||||
|
socket.onopen = () => {
|
||||||
|
addMessage('Connected to sensor service');
|
||||||
|
updateStatus('Connected to Sensor Service');
|
||||||
|
isConnected = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
if (data.type !== 'connection_established') {
|
||||||
|
addMessage('Data: ' + JSON.stringify(data));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
addMessage('Raw: ' + event.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onclose = (event) => {
|
||||||
|
addMessage(`Sensor service connection closed. Code: ${event.code}`);
|
||||||
|
updateStatus('Disconnected');
|
||||||
|
isConnected = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onerror = (error) => {
|
||||||
|
addMessage('Sensor service error: ' + error);
|
||||||
|
updateStatus('Error');
|
||||||
|
isConnected = false;
|
||||||
|
};
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
addMessage('Raw: ' + event.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onclose = (event) => {
|
||||||
|
addMessage(`API Gateway connection closed. Code: ${event.code}`);
|
||||||
|
updateStatus('Disconnected');
|
||||||
|
isConnected = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onerror = (error) => {
|
||||||
|
addMessage('API Gateway error: ' + error);
|
||||||
|
updateStatus('Error');
|
||||||
|
isConnected = false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function disconnect() {
|
||||||
|
if (socket) {
|
||||||
|
socket.close();
|
||||||
|
socket = null;
|
||||||
|
isConnected = false;
|
||||||
|
updateStatus('Disconnected');
|
||||||
|
addMessage('Disconnected manually');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user