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,
|
||||
refreshInterval: 5,
|
||||
dateFormat: 'relative',
|
||||
timeFormat: '12h'
|
||||
timeFormat: '12h',
|
||||
},
|
||||
notifications: {
|
||||
enabled: true,
|
||||
sound: true,
|
||||
desktop: false,
|
||||
email: false,
|
||||
criticalOnly: false
|
||||
criticalOnly: false,
|
||||
},
|
||||
autoConnect: true,
|
||||
websocketUrl: 'ws://192.168.1.73:8000/ws',
|
||||
developerMode: false
|
||||
websocketUrl: 'ws://localhost:8000/ws',
|
||||
developerMode: false,
|
||||
}
|
||||
|
||||
export const useSettingsStore = defineStore('settings', () => {
|
||||
@@ -206,10 +206,14 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
|
||||
// Auto-save on changes (debounced)
|
||||
let saveTimeout: number | undefined
|
||||
watch(settings, () => {
|
||||
if (saveTimeout) clearTimeout(saveTimeout)
|
||||
saveTimeout = window.setTimeout(saveSettings, 500)
|
||||
}, { deep: true })
|
||||
watch(
|
||||
settings,
|
||||
() => {
|
||||
if (saveTimeout) clearTimeout(saveTimeout)
|
||||
saveTimeout = window.setTimeout(saveSettings, 500)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
}
|
||||
|
||||
// Get available languages
|
||||
@@ -218,7 +222,7 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
{ code: 'en', name: 'English', nativeName: 'English' },
|
||||
{ code: 'es', name: 'Spanish', nativeName: 'Español' },
|
||||
{ code: 'fr', name: 'French', nativeName: 'Français' },
|
||||
{ code: 'de', name: 'German', nativeName: 'Deutsch' }
|
||||
{ code: 'de', name: 'German', nativeName: 'Deutsch' },
|
||||
]
|
||||
}
|
||||
|
||||
@@ -227,7 +231,7 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
return [
|
||||
{ value: 'system', label: 'System Default', icon: '🔄' },
|
||||
{ value: 'light', label: 'Light Mode', icon: '☀️' },
|
||||
{ value: 'dark', label: 'Dark Mode', icon: '🌙' }
|
||||
{ value: 'dark', label: 'Dark Mode', icon: '🌙' },
|
||||
]
|
||||
}
|
||||
|
||||
@@ -238,20 +242,20 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
value: 'hover',
|
||||
label: 'Show on Hover',
|
||||
description: 'Navigation appears when hovering near bottom (desktop only)',
|
||||
icon: '👆'
|
||||
icon: '👆',
|
||||
},
|
||||
{
|
||||
value: 'always',
|
||||
label: 'Always Visible',
|
||||
description: 'Navigation is permanently visible',
|
||||
icon: '👁️'
|
||||
icon: '👁️',
|
||||
},
|
||||
{
|
||||
value: 'hidden',
|
||||
label: 'Hidden',
|
||||
description: 'Navigation is completely hidden',
|
||||
icon: '🫥'
|
||||
}
|
||||
icon: '🫥',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -277,6 +281,6 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
// Getters
|
||||
getAvailableLanguages,
|
||||
getThemeOptions,
|
||||
getNavigationModeOptions
|
||||
getNavigationModeOptions,
|
||||
}
|
||||
})
|
||||
@@ -4,7 +4,9 @@
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<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 class="mt-4 sm:mt-0">
|
||||
<div class="flex items-center gap-2 text-sm text-gray-600">
|
||||
@@ -96,10 +98,18 @@
|
||||
<button
|
||||
@click="toggleOptimization('hvac', 'predictive_temp')"
|
||||
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"
|
||||
:class="isOptimizationEnabled('hvac', 'predictive_temp') ? 'translate-x-5' : 'translate-x-1'"></span>
|
||||
<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>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -107,10 +117,20 @@
|
||||
<button
|
||||
@click="toggleOptimization('hvac', 'occupancy_schedule')"
|
||||
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"
|
||||
:class="isOptimizationEnabled('hvac', 'occupancy_schedule') ? 'translate-x-5' : 'translate-x-1'"></span>
|
||||
<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>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -118,10 +138,18 @@
|
||||
<button
|
||||
@click="toggleOptimization('hvac', 'weather_adjust')"
|
||||
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"
|
||||
:class="isOptimizationEnabled('hvac', 'weather_adjust') ? 'translate-x-5' : 'translate-x-1'"></span>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -166,10 +194,20 @@
|
||||
<button
|
||||
@click="toggleOptimization('lighting', 'daylight_harvest')"
|
||||
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"
|
||||
:class="isOptimizationEnabled('lighting', 'daylight_harvest') ? 'translate-x-5' : 'translate-x-1'"></span>
|
||||
<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>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -177,10 +215,20 @@
|
||||
<button
|
||||
@click="toggleOptimization('lighting', 'motion_control')"
|
||||
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"
|
||||
:class="isOptimizationEnabled('lighting', 'motion_control') ? 'translate-x-5' : 'translate-x-1'"></span>
|
||||
<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>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -188,10 +236,20 @@
|
||||
<button
|
||||
@click="toggleOptimization('lighting', 'circadian_sync')"
|
||||
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"
|
||||
:class="isOptimizationEnabled('lighting', 'circadian_sync') ? 'translate-x-5' : 'translate-x-1'"></span>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -223,7 +281,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<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
|
||||
</span>
|
||||
</div>
|
||||
@@ -236,10 +296,20 @@
|
||||
<button
|
||||
@click="toggleOptimization('security', 'adaptive_access')"
|
||||
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"
|
||||
:class="isOptimizationEnabled('security', 'adaptive_access') ? 'translate-x-5' : 'translate-x-1'"></span>
|
||||
<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>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -247,10 +317,20 @@
|
||||
<button
|
||||
@click="toggleOptimization('security', 'anomaly_detection')"
|
||||
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"
|
||||
:class="isOptimizationEnabled('security', 'anomaly_detection') ? 'translate-x-5' : 'translate-x-1'"></span>
|
||||
<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>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -258,10 +338,20 @@
|
||||
<button
|
||||
@click="toggleOptimization('security', 'predictive_maintenance')"
|
||||
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"
|
||||
:class="isOptimizationEnabled('security', 'predictive_maintenance') ? 'translate-x-5' : 'translate-x-1'"></span>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -306,10 +396,20 @@
|
||||
<button
|
||||
@click="toggleOptimization('air_quality', 'co2_ventilation')"
|
||||
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"
|
||||
:class="isOptimizationEnabled('air_quality', 'co2_ventilation') ? 'translate-x-5' : 'translate-x-1'"></span>
|
||||
<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>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -317,10 +417,20 @@
|
||||
<button
|
||||
@click="toggleOptimization('air_quality', 'smart_filtration')"
|
||||
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"
|
||||
:class="isOptimizationEnabled('air_quality', 'smart_filtration') ? 'translate-x-5' : 'translate-x-1'"></span>
|
||||
<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>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -328,10 +438,20 @@
|
||||
<button
|
||||
@click="toggleOptimization('air_quality', 'humidity_control')"
|
||||
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"
|
||||
:class="isOptimizationEnabled('air_quality', 'humidity_control') ? 'translate-x-5' : 'translate-x-1'"></span>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -379,7 +499,9 @@
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-xs text-gray-600">
|
||||
<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>
|
||||
@@ -415,9 +537,7 @@
|
||||
{{ schedule.frequency }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-600">
|
||||
Next run: {{ schedule.nextRun }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-600">Next run: {{ schedule.nextRun }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 border-t border-gray-100">
|
||||
@@ -432,20 +552,29 @@
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<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>
|
||||
<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="text-sm text-gray-600">Create room-specific temperature schedules</div>
|
||||
</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="text-sm text-gray-600">Distribute energy usage across peak times</div>
|
||||
</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="text-sm text-gray-600">AI-driven equipment monitoring</div>
|
||||
</button>
|
||||
@@ -483,23 +612,23 @@ const optimizations = ref({
|
||||
hvac: {
|
||||
predictive_temp: true,
|
||||
occupancy_schedule: true,
|
||||
weather_adjust: false
|
||||
weather_adjust: false,
|
||||
},
|
||||
lighting: {
|
||||
daylight_harvest: true,
|
||||
motion_control: true,
|
||||
circadian_sync: false
|
||||
circadian_sync: false,
|
||||
},
|
||||
security: {
|
||||
adaptive_access: false,
|
||||
anomaly_detection: true,
|
||||
predictive_maintenance: false
|
||||
predictive_maintenance: false,
|
||||
},
|
||||
air_quality: {
|
||||
co2_ventilation: true,
|
||||
smart_filtration: true,
|
||||
humidity_control: false
|
||||
}
|
||||
humidity_control: false,
|
||||
},
|
||||
})
|
||||
|
||||
// Mock data for active optimizations
|
||||
@@ -512,7 +641,7 @@ const activeOptimizations = ref([
|
||||
statusColor: 'bg-green-100',
|
||||
startTime: '2 hours ago',
|
||||
impact: '-15.2% energy',
|
||||
impactColor: 'text-green-600'
|
||||
impactColor: 'text-green-600',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
@@ -522,7 +651,7 @@ const activeOptimizations = ref([
|
||||
statusColor: 'bg-amber-100',
|
||||
startTime: '45 minutes ago',
|
||||
impact: '-8.7% energy',
|
||||
impactColor: 'text-green-600'
|
||||
impactColor: 'text-green-600',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
@@ -532,8 +661,8 @@ const activeOptimizations = ref([
|
||||
statusColor: 'bg-blue-100',
|
||||
startTime: '1 hour ago',
|
||||
impact: '+2.1% comfort',
|
||||
impactColor: 'text-blue-600'
|
||||
}
|
||||
impactColor: 'text-blue-600',
|
||||
},
|
||||
])
|
||||
|
||||
// Mock data for upcoming optimizations
|
||||
@@ -544,7 +673,7 @@ const upcomingOptimizations = ref([
|
||||
icon: '⚡',
|
||||
rooms: ['All Zones'],
|
||||
frequency: 'Daily',
|
||||
nextRun: 'Today, 2:00 PM'
|
||||
nextRun: 'Today, 2:00 PM',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
@@ -552,7 +681,7 @@ const upcomingOptimizations = ref([
|
||||
icon: '🌙',
|
||||
rooms: ['Office Floor 1', 'Office Floor 2'],
|
||||
frequency: 'Daily',
|
||||
nextRun: 'Today, 6:00 PM'
|
||||
nextRun: 'Today, 6:00 PM',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
@@ -560,8 +689,8 @@ const upcomingOptimizations = ref([
|
||||
icon: '🏢',
|
||||
rooms: ['Conference Rooms', 'Meeting Rooms'],
|
||||
frequency: 'Weekly',
|
||||
nextRun: 'Saturday, 8:00 AM'
|
||||
}
|
||||
nextRun: 'Saturday, 8:00 AM',
|
||||
},
|
||||
])
|
||||
|
||||
// Computed properties
|
||||
@@ -582,7 +711,7 @@ const toggleOptimization = (category: string, optimization: string) => {
|
||||
const current = isOptimizationEnabled(category, optimization)
|
||||
const categorySettings = optimizations.value[category as keyof typeof optimizations.value]
|
||||
if (categorySettings) {
|
||||
(categorySettings as any)[optimization] = !current
|
||||
;(categorySettings as any)[optimization] = !current
|
||||
}
|
||||
|
||||
// Simulate API call
|
||||
@@ -590,7 +719,7 @@ const toggleOptimization = (category: string, optimization: string) => {
|
||||
}
|
||||
|
||||
const pauseOptimization = (id: number) => {
|
||||
const optimization = activeOptimizations.value.find(opt => opt.id === id)
|
||||
const optimization = activeOptimizations.value.find((opt) => opt.id === id)
|
||||
if (optimization) {
|
||||
console.log(`Pausing optimization: ${optimization.name}`)
|
||||
// In a real app, this would make an API call
|
||||
@@ -600,7 +729,7 @@ const pauseOptimization = (id: number) => {
|
||||
// Initialize connection
|
||||
onMounted(() => {
|
||||
if (!energyStore.isConnected) {
|
||||
energyStore.connect('ws://192.168.1.73:8000/ws')
|
||||
energyStore.connect('ws://localhost:8000/ws')
|
||||
}
|
||||
})
|
||||
</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