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:
rafaeldpsilva
2025-09-18 14:29:36 +01:00
parent 32c63628b6
commit faed09d3b6
10 changed files with 1498 additions and 114 deletions

View 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
View 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
}
}

View 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
View 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
View 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
View 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
View 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
}
})

View File

@@ -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,
} }
}) })

View File

@@ -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
View 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>