This commit is contained in:
rafaeldpsilva
2025-12-20 00:17:21 +00:00
parent 4b4338fb91
commit c3364cc422
31 changed files with 818 additions and 425 deletions

View File

@@ -30,7 +30,12 @@
}
// Grid utilities
@mixin grid-responsive($columns-mobile: 1, $columns-tablet: 2, $columns-desktop: 3, $gap: $spacing-md) {
@mixin grid-responsive(
$columns-mobile: 1,
$columns-tablet: 2,
$columns-desktop: 3,
$gap: $spacing-md
) {
display: grid;
gap: $gap;
grid-template-columns: repeat($columns-mobile, 1fr);
@@ -178,4 +183,4 @@
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}

View File

@@ -66,9 +66,15 @@ $radius-2xl: 1.5rem;
// Shadows
$shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
$shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
$shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
$shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
$shadow-md:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
$shadow-lg:
0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
$shadow-xl:
0 20px 25px -5px rgba(0, 0, 0, 0.1),
0 10px 10px -5px rgba(0, 0, 0, 0.04);
// Transitions
$transition-fast: 150ms ease-in-out;
@@ -80,4 +86,4 @@ $breakpoint-sm: 640px;
$breakpoint-md: 768px;
$breakpoint-lg: 1024px;
$breakpoint-xl: 1280px;
$breakpoint-2xl: 1536px;
$breakpoint-2xl: 1536px;

View File

@@ -1,5 +1,10 @@
// Typography Styles
h1, h2, h3, h4, h5, h6 {
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 0;
font-weight: 600;
}
@@ -40,17 +45,41 @@ p {
}
// Text utilities
.text-xs { font-size: 0.75rem; }
.text-sm { font-size: 0.875rem; }
.text-base { font-size: 1rem; }
.text-lg { font-size: 1.125rem; }
.text-xl { font-size: 1.25rem; }
.text-2xl { font-size: 1.5rem; }
.text-xs {
font-size: 0.75rem;
}
.text-sm {
font-size: 0.875rem;
}
.text-base {
font-size: 1rem;
}
.text-lg {
font-size: 1.125rem;
}
.text-xl {
font-size: 1.25rem;
}
.text-2xl {
font-size: 1.5rem;
}
.font-medium { font-weight: 500; }
.font-semibold { font-weight: 600; }
.font-bold { font-weight: 700; }
.font-medium {
font-weight: 500;
}
.font-semibold {
font-weight: 600;
}
.font-bold {
font-weight: 700;
}
.text-center { text-align: center; }
.text-left { text-align: left; }
.text-right { text-align: right; }
.text-center {
text-align: center;
}
.text-left {
text-align: left;
}
.text-right {
text-align: right;
}

View File

@@ -130,4 +130,4 @@
height: 1.25rem;
color: $gray-400;
}
}
}

View File

@@ -449,4 +449,4 @@
.sensor-security {
@include sensor-type-style($sensor-security-bg, $sensor-security-text);
}
}

View File

@@ -44,4 +44,4 @@
// Custom Range Input
.range-slider {
@include custom-slider;
}
}

View File

@@ -253,4 +253,4 @@
}
}
}
}
}

View File

@@ -24,4 +24,4 @@
margin-top: $spacing-xl;
}
}
}
}

View File

@@ -2,14 +2,28 @@
.grid {
display: grid;
&--1 { grid-template-columns: 1fr; }
&--2 { grid-template-columns: repeat(2, 1fr); }
&--3 { grid-template-columns: repeat(3, 1fr); }
&--4 { grid-template-columns: repeat(4, 1fr); }
&--1 {
grid-template-columns: 1fr;
}
&--2 {
grid-template-columns: repeat(2, 1fr);
}
&--3 {
grid-template-columns: repeat(3, 1fr);
}
&--4 {
grid-template-columns: repeat(4, 1fr);
}
&--gap-2 { gap: $spacing-sm; }
&--gap-4 { gap: $spacing-md; }
&--gap-6 { gap: $spacing-lg; }
&--gap-2 {
gap: $spacing-sm;
}
&--gap-4 {
gap: $spacing-md;
}
&--gap-6 {
gap: $spacing-lg;
}
// Responsive grids
&--responsive-simple {
@@ -45,8 +59,16 @@
flex-wrap: wrap;
}
&--gap-1 { gap: $spacing-xs; }
&--gap-2 { gap: $spacing-sm; }
&--gap-3 { gap: $spacing-md; }
&--gap-4 { gap: $spacing-lg; }
}
&--gap-1 {
gap: $spacing-xs;
}
&--gap-2 {
gap: $spacing-sm;
}
&--gap-3 {
gap: $spacing-md;
}
&--gap-4 {
gap: $spacing-lg;
}
}

View File

@@ -164,4 +164,4 @@
height: 2rem;
// Chart styling handled by chart library
}
}
}

View File

@@ -111,4 +111,4 @@
color: $gray-600;
}
}
}
}

View File

@@ -12,98 +12,226 @@
}
// Spacing utilities
.space-y-2 > * + * { margin-top: $spacing-sm; }
.space-y-3 > * + * { margin-top: $spacing-md; }
.space-y-4 > * + * { margin-top: $spacing-lg; }
.space-y-6 > * + * { margin-top: $spacing-xl; }
.space-y-2 > * + * {
margin-top: $spacing-sm;
}
.space-y-3 > * + * {
margin-top: $spacing-md;
}
.space-y-4 > * + * {
margin-top: $spacing-lg;
}
.space-y-6 > * + * {
margin-top: $spacing-xl;
}
.space-x-2 > * + * { margin-left: $spacing-sm; }
.space-x-3 > * + * { margin-left: $spacing-md; }
.space-x-4 > * + * { margin-left: $spacing-lg; }
.space-x-2 > * + * {
margin-left: $spacing-sm;
}
.space-x-3 > * + * {
margin-left: $spacing-md;
}
.space-x-4 > * + * {
margin-left: $spacing-lg;
}
// Margin utilities
.m-0 { margin: 0; }
.mb-1 { margin-bottom: $spacing-xs; }
.mb-2 { margin-bottom: $spacing-sm; }
.mb-3 { margin-bottom: $spacing-md; }
.mb-4 { margin-bottom: $spacing-lg; }
.m-0 {
margin: 0;
}
.mb-1 {
margin-bottom: $spacing-xs;
}
.mb-2 {
margin-bottom: $spacing-sm;
}
.mb-3 {
margin-bottom: $spacing-md;
}
.mb-4 {
margin-bottom: $spacing-lg;
}
.mt-1 { margin-top: $spacing-xs; }
.mt-2 { margin-top: $spacing-sm; }
.mt-3 { margin-top: $spacing-md; }
.mt-4 { margin-top: $spacing-lg; }
.mt-1 {
margin-top: $spacing-xs;
}
.mt-2 {
margin-top: $spacing-sm;
}
.mt-3 {
margin-top: $spacing-md;
}
.mt-4 {
margin-top: $spacing-lg;
}
// Padding utilities
.p-0 { padding: 0; }
.p-1 { padding: $spacing-xs; }
.p-2 { padding: $spacing-sm; }
.p-3 { padding: $spacing-md; }
.p-4 { padding: $spacing-lg; }
// Padding utilities
.p-0 {
padding: 0;
}
.p-1 {
padding: $spacing-xs;
}
.p-2 {
padding: $spacing-sm;
}
.p-3 {
padding: $spacing-md;
}
.p-4 {
padding: $spacing-lg;
}
// Width utilities
.w-full { width: 100%; }
.w-auto { width: auto; }
.w-full {
width: 100%;
}
.w-auto {
width: auto;
}
// Height utilities
.h-full { height: 100%; }
.h-auto { height: auto; }
.h-full {
height: 100%;
}
.h-auto {
height: auto;
}
// Color utilities
.text-primary { color: $primary; }
.text-secondary { color: $secondary; }
.text-success { color: $success; }
.text-warning { color: $warning; }
.text-danger { color: $danger; }
.text-primary {
color: $primary;
}
.text-secondary {
color: $secondary;
}
.text-success {
color: $success;
}
.text-warning {
color: $warning;
}
.text-danger {
color: $danger;
}
.text-gray-400 { color: $gray-400; }
.text-gray-500 { color: $gray-500; }
.text-gray-600 { color: $gray-600; }
.text-gray-700 { color: $gray-700; }
.text-gray-900 { color: $gray-900; }
.text-gray-400 {
color: $gray-400;
}
.text-gray-500 {
color: $gray-500;
}
.text-gray-600 {
color: $gray-600;
}
.text-gray-700 {
color: $gray-700;
}
.text-gray-900 {
color: $gray-900;
}
// Background utilities
.bg-primary { background-color: $primary; }
.bg-white { background-color: white; }
.bg-gray-50 { background-color: $gray-50; }
.bg-gray-100 { background-color: $gray-100; }
.bg-primary {
background-color: $primary;
}
.bg-white {
background-color: white;
}
.bg-gray-50 {
background-color: $gray-50;
}
.bg-gray-100 {
background-color: $gray-100;
}
// Border utilities
.border { border: 1px solid $gray-200; }
.border-0 { border: 0; }
.border-gray-100 { border-color: $gray-100; }
.border-gray-200 { border-color: $gray-200; }
.border {
border: 1px solid $gray-200;
}
.border-0 {
border: 0;
}
.border-gray-100 {
border-color: $gray-100;
}
.border-gray-200 {
border-color: $gray-200;
}
// Border radius utilities
.rounded { border-radius: $radius-md; }
.rounded-lg { border-radius: $radius-lg; }
.rounded-xl { border-radius: $radius-xl; }
.rounded-2xl { border-radius: $radius-2xl; }
.rounded-full { border-radius: 9999px; }
.rounded {
border-radius: $radius-md;
}
.rounded-lg {
border-radius: $radius-lg;
}
.rounded-xl {
border-radius: $radius-xl;
}
.rounded-2xl {
border-radius: $radius-2xl;
}
.rounded-full {
border-radius: 9999px;
}
// Shadow utilities
.shadow-sm { box-shadow: $shadow-sm; }
.shadow-md { box-shadow: $shadow-md; }
.shadow-lg { box-shadow: $shadow-lg; }
.shadow-xl { box-shadow: $shadow-xl; }
.shadow-sm {
box-shadow: $shadow-sm;
}
.shadow-md {
box-shadow: $shadow-md;
}
.shadow-lg {
box-shadow: $shadow-lg;
}
.shadow-xl {
box-shadow: $shadow-xl;
}
// Position utilities
.relative { position: relative; }
.absolute { position: absolute; }
.fixed { position: fixed; }
.relative {
position: relative;
}
.absolute {
position: absolute;
}
.fixed {
position: fixed;
}
// Z-index utilities
.z-10 { z-index: 10; }
.z-20 { z-index: 20; }
.z-50 { z-index: 50; }
.z-10 {
z-index: 10;
}
.z-20 {
z-index: 20;
}
.z-50 {
z-index: 50;
}
// Overflow utilities
.overflow-hidden { overflow: hidden; }
.overflow-y-auto { overflow-y: auto; }
.overflow-hidden {
overflow: hidden;
}
.overflow-y-auto {
overflow-y: auto;
}
// Cursor utilities
.cursor-pointer { cursor: pointer; }
.cursor-not-allowed { cursor: not-allowed; }
.cursor-pointer {
cursor: pointer;
}
.cursor-not-allowed {
cursor: not-allowed;
}
// Opacity utilities
.opacity-50 { opacity: 0.5; }
.opacity-75 { opacity: 0.75; }
.opacity-50 {
opacity: 0.5;
}
.opacity-75 {
opacity: 0.75;
}

View File

@@ -1,7 +1,7 @@
<template>
<div class="bg-white rounded-2xl shadow-sm p-4">
<h6 class="text-sm font-bold text-gray-500 mb-4">Air Quality Status</h6>
<!-- Overall Status -->
<div class="mb-4 p-3 rounded-lg" :class="getOverallStatusBg()">
<div class="flex items-center justify-between">
@@ -13,11 +13,28 @@
Building Average: {{ overallCO2.toFixed(0) }} ppm
</div>
</div>
<div class="w-12 h-12 rounded-full flex items-center justify-center" :class="getOverallStatusIconBg()">
<svg class="w-6 h-6" :class="getOverallStatusText()" fill="currentColor" viewBox="0 0 24 24">
<path d="M12,2C6.5,2 2,6.5 2,12C2,17.5 6.5,22 12,22C17.5,22 22,17.5 22,12C22,6.5 17.5,2 12,2M10,16.5L6,12.5L7.5,11L10,13.5L16.5,7L18,8.5L10,16.5Z" v-if="overallStatus === 'good'"/>
<path d="M12,2C6.5,2 2,6.5 2,12C2,17.5 6.5,22 12,22C17.5,22 22,17.5 22,12C22,6.5 17.5,2 12,2M12,7L17,12L12,17L7,12L12,7Z" v-else-if="overallStatus === 'moderate'"/>
<path d="M12,2C6.5,2 2,6.5 2,12C2,17.5 6.5,22 12,22C17.5,22 22,17.5 22,12C22,6.5 17.5,2 12,2M12,7L17,12L12,17L7,12L12,7Z" v-else/>
<div
class="w-12 h-12 rounded-full flex items-center justify-center"
:class="getOverallStatusIconBg()"
>
<svg
class="w-6 h-6"
:class="getOverallStatusText()"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
d="M12,2C6.5,2 2,6.5 2,12C2,17.5 6.5,22 12,22C17.5,22 22,17.5 22,12C22,6.5 17.5,2 12,2M10,16.5L6,12.5L7.5,11L10,13.5L16.5,7L18,8.5L10,16.5Z"
v-if="overallStatus === 'good'"
/>
<path
d="M12,2C6.5,2 2,6.5 2,12C2,17.5 6.5,22 12,22C17.5,22 22,17.5 22,12C22,6.5 17.5,2 12,2M12,7L17,12L12,17L7,12L12,7Z"
v-else-if="overallStatus === 'moderate'"
/>
<path
d="M12,2C6.5,2 2,6.5 2,12C2,17.5 6.5,22 12,22C17.5,22 22,17.5 22,12C22,6.5 17.5,2 12,2M12,7L17,12L12,17L7,12L12,7Z"
v-else
/>
</svg>
</div>
</div>
@@ -28,8 +45,12 @@
<div v-if="roomsList.length === 0" class="text-center text-gray-500 py-4">
No air quality data available
</div>
<div v-for="room in roomsList" :key="room.room" class="flex items-center justify-between p-2 rounded">
<div
v-for="room in roomsList"
:key="room.room"
class="flex items-center justify-between p-2 rounded"
>
<div class="flex items-center gap-2">
<div
class="w-3 h-3 rounded-full"
@@ -76,9 +97,9 @@ const roomStore = useRoomStore()
const roomsList = computed(() => {
return Array.from(roomStore.roomsData.values())
.filter(room => room.co2) // Only include rooms with CO2 data
.sort((a, b) =>
(b.co2?.current || 0) - (a.co2?.current || 0) // Sort by CO2 level descending
.filter((room) => room.co2) // Only include rooms with CO2 data
.sort(
(a, b) => (b.co2?.current || 0) - (a.co2?.current || 0), // Sort by CO2 level descending
)
})
@@ -93,23 +114,27 @@ const overallStatus = computed(() => {
})
const roomsWithGoodAir = computed(() => {
return roomsList.value.filter(room => room.co2?.status === 'good').length
return roomsList.value.filter((room) => room.co2?.status === 'good').length
})
const roomsNeedingAttention = computed(() => {
return roomsList.value.filter(room => room.co2?.status && ['poor', 'critical'].includes(room.co2.status)).length
return roomsList.value.filter(
(room) => room.co2?.status && ['poor', 'critical'].includes(room.co2.status),
).length
})
const recommendations = computed(() => {
const recs = []
const criticalRooms = roomsList.value.filter(room => room.co2?.status === 'critical')
const poorRooms = roomsList.value.filter(room => room.co2?.status === 'poor')
const criticalRooms = roomsList.value.filter((room) => room.co2?.status === 'critical')
const poorRooms = roomsList.value.filter((room) => room.co2?.status === 'poor')
if (criticalRooms.length > 0) {
recs.push(`Immediate ventilation needed in ${criticalRooms[0].room}`)
}
if (poorRooms.length > 0) {
recs.push(`Increase air circulation in ${poorRooms.length} room${poorRooms.length > 1 ? 's' : ''}`)
recs.push(
`Increase air circulation in ${poorRooms.length} room${poorRooms.length > 1 ? 's' : ''}`,
)
}
if (overallCO2.value > 800) {
recs.push('Consider adjusting HVAC settings building-wide')
@@ -120,61 +145,91 @@ const recommendations = computed(() => {
const getOverallStatus = () => {
switch (overallStatus.value) {
case 'good': return 'Excellent Air Quality'
case 'moderate': return 'Moderate Air Quality'
case 'poor': return 'Poor Air Quality'
case 'critical': return 'Critical - Action Required'
default: return 'Unknown Status'
case 'good':
return 'Excellent Air Quality'
case 'moderate':
return 'Moderate Air Quality'
case 'poor':
return 'Poor Air Quality'
case 'critical':
return 'Critical - Action Required'
default:
return 'Unknown Status'
}
}
const getOverallStatusBg = () => {
switch (overallStatus.value) {
case 'good': return 'bg-green-50 border border-green-200'
case 'moderate': return 'bg-yellow-50 border border-yellow-200'
case 'poor': return 'bg-orange-50 border border-orange-200'
case 'critical': return 'bg-red-50 border border-red-200'
default: return 'bg-gray-50 border border-gray-200'
case 'good':
return 'bg-green-50 border border-green-200'
case 'moderate':
return 'bg-yellow-50 border border-yellow-200'
case 'poor':
return 'bg-orange-50 border border-orange-200'
case 'critical':
return 'bg-red-50 border border-red-200'
default:
return 'bg-gray-50 border border-gray-200'
}
}
const getOverallStatusText = () => {
switch (overallStatus.value) {
case 'good': return 'text-green-700'
case 'moderate': return 'text-yellow-700'
case 'poor': return 'text-orange-700'
case 'critical': return 'text-red-700'
default: return 'text-gray-700'
case 'good':
return 'text-green-700'
case 'moderate':
return 'text-yellow-700'
case 'poor':
return 'text-orange-700'
case 'critical':
return 'text-red-700'
default:
return 'text-gray-700'
}
}
const getOverallStatusIconBg = () => {
switch (overallStatus.value) {
case 'good': return 'bg-green-100'
case 'moderate': return 'bg-yellow-100'
case 'poor': return 'bg-orange-100'
case 'critical': return 'bg-red-100'
default: return 'bg-gray-100'
case 'good':
return 'bg-green-100'
case 'moderate':
return 'bg-yellow-100'
case 'poor':
return 'bg-orange-100'
case 'critical':
return 'bg-red-100'
default:
return 'bg-gray-100'
}
}
const getCO2StatusColor = (status: string) => {
switch (status) {
case 'good': return 'bg-green-500'
case 'moderate': return 'bg-yellow-500'
case 'poor': return 'bg-orange-500'
case 'critical': return 'bg-red-500'
default: return 'bg-gray-500'
case 'good':
return 'bg-green-500'
case 'moderate':
return 'bg-yellow-500'
case 'poor':
return 'bg-orange-500'
case 'critical':
return 'bg-red-500'
default:
return 'bg-gray-500'
}
}
const getCO2TextColor = (status: string) => {
switch (status) {
case 'good': return 'text-green-600'
case 'moderate': return 'text-yellow-600'
case 'poor': return 'text-orange-600'
case 'critical': return 'text-red-600'
default: return 'text-gray-600'
case 'good':
return 'text-green-600'
case 'moderate':
return 'text-yellow-600'
case 'poor':
return 'text-orange-600'
case 'critical':
return 'text-red-600'
default:
return 'text-gray-600'
}
}
</script>
</script>

View File

@@ -17,7 +17,7 @@
class="w-2 h-2 rounded-full transition-all duration-300"
:class="[
getSensorStatusColor(sensor.status),
isRecentlyUpdated ? 'animate-pulse shadow-lg shadow-green-400/50' : ''
isRecentlyUpdated ? 'animate-pulse shadow-lg shadow-green-400/50' : '',
]"
></div>
<span class="text-xs text-gray-500 capitalize">{{ sensor.status }}</span>
@@ -32,7 +32,9 @@
<label class="block text-sm font-medium text-gray-700 mb-2">Room Assignment</label>
<select
:value="sensor.room"
@change="$emit('updateRoom', sensor.sensor_id, ($event.target as HTMLSelectElement).value)"
@change="
$emit('updateRoom', sensor.sensor_id, ($event.target as HTMLSelectElement).value)
"
class="w-full px-3 py-2 border border-gray-200 rounded-lg bg-white text-sm"
>
<option value="">Unassigned</option>
@@ -46,7 +48,7 @@
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Tags</label>
<div class="flex flex-wrap gap-1 mb-2">
<span
<span
v-for="tag in sensor.tags || getDefaultTags(sensor)"
:key="tag"
class="px-2 py-1 bg-gray-100 text-gray-700 rounded-full text-xs font-medium"
@@ -60,7 +62,7 @@
<div>
<div class="text-sm font-medium text-gray-700 mb-2">Monitoring Capabilities</div>
<div class="flex flex-wrap gap-1">
<span
<span
v-for="capability in sensor.capabilities.monitoring"
:key="capability"
class="px-2 py-1 bg-blue-100 text-blue-700 rounded text-xs font-medium"
@@ -74,8 +76,7 @@
<div>
<div class="text-sm font-medium text-gray-700 mb-2">Current Values</div>
<div class="grid grid-cols-2 gap-2 text-xs">
<div v-for="metric in sensorValues" :key="metric.type"
class="bg-gray-50 rounded p-2">
<div v-for="metric in sensorValues" :key="metric.type" class="bg-gray-50 rounded p-2">
<div class="text-gray-600 mb-1">{{ metric.label }}</div>
<div class="font-medium text-gray-900">
{{ metric.value }} <span class="text-gray-500">{{ metric.unit }}</span>
@@ -109,7 +110,7 @@
<div class="flex items-center gap-1">
<span>{{ sensor.metadata.battery }}%</span>
<div class="w-3 h-1 bg-gray-200 rounded-full">
<div
<div
class="h-full rounded-full transition-all"
:class="getBatteryColor(sensor.metadata.battery)"
:style="{ width: sensor.metadata.battery + '%' }"
@@ -122,11 +123,11 @@
<div class="flex items-center gap-1">
<span>{{ sensor.metadata.signalStrength }}%</span>
<div class="flex gap-0.5">
<div
v-for="i in 4"
<div
v-for="i in 4"
:key="i"
class="w-1 h-2 bg-gray-200 rounded-sm"
:class="{ 'bg-green-500': (sensor.metadata.signalStrength / 25) >= i }"
:class="{ 'bg-green-500': sensor.metadata.signalStrength / 25 >= i }"
></div>
</div>
</div>
@@ -155,7 +156,9 @@
<!-- No Actions State -->
<div v-else>
<div class="text-sm font-medium text-gray-700 mb-2">Device Actions</div>
<div class="text-xs text-gray-500 text-center py-3 bg-gray-50 rounded border-2 border-dashed border-gray-200">
<div
class="text-xs text-gray-500 text-center py-3 bg-gray-50 rounded border-2 border-dashed border-gray-200"
>
This device is monitor-only and has no available actions
</div>
</div>
@@ -186,7 +189,10 @@ const getSensorValues = (sensor: SensorDevice) => {
// Get real-time sensor reading from store
const latestReading = sensorStore.latestReadings.get(sensor.sensor_id)
console.log(`[Detailed] Getting values for sensor ${sensor.sensor_id}, found reading:`, latestReading)
console.log(
`[Detailed] Getting values for sensor ${sensor.sensor_id}, found reading:`,
latestReading,
)
console.log('[Detailed] Available readings:', Array.from(sensorStore.latestReadings.keys()))
console.log(`[Detailed] Sensor capabilities:`, sensor.capabilities?.monitoring)
@@ -197,7 +203,7 @@ const getSensorValues = (sensor: SensorDevice) => {
type: 'energy',
label: 'Energy Consumption',
value: energyValue,
unit: latestReading?.energy?.unit || 'kWh'
unit: latestReading?.energy?.unit || 'kWh',
})
}
@@ -208,19 +214,19 @@ const getSensorValues = (sensor: SensorDevice) => {
type: 'co2',
label: 'CO2 Level',
value: co2Value,
unit: latestReading?.co2?.unit || 'ppm'
unit: latestReading?.co2?.unit || 'ppm',
})
}
// Only show temperature if the sensor monitors temperature
if (sensor.capabilities?.monitoring?.includes('temperature')) {
const tempValue = latestReading?.temperature?.value?.toFixed(1) ||
(Math.random() * 8 + 18).toFixed(1)
const tempValue =
latestReading?.temperature?.value?.toFixed(1) || (Math.random() * 8 + 18).toFixed(1)
values.push({
type: 'temperature',
label: 'Temperature',
value: tempValue,
unit: latestReading?.temperature?.unit || '°C'
unit: latestReading?.temperature?.unit || '°C',
})
}
@@ -230,7 +236,7 @@ const getSensorValues = (sensor: SensorDevice) => {
type: 'humidity',
label: 'Humidity',
value: Math.floor(Math.random() * 40 + 30),
unit: '%'
unit: '%',
})
}
@@ -240,7 +246,7 @@ const getSensorValues = (sensor: SensorDevice) => {
type: 'motion',
label: 'Motion Status',
value: Math.random() > 0.7 ? 'Detected' : 'Clear',
unit: ''
unit: '',
})
}
@@ -252,13 +258,13 @@ const getSensorValues = (sensor: SensorDevice) => {
type: 'brightness',
label: 'Brightness Level',
value: Math.floor(Math.random() * 100),
unit: '%'
unit: '%',
})
values.push({
type: 'power',
label: 'Power Draw',
value: Math.floor(Math.random() * 50 + 5),
unit: 'W'
unit: 'W',
})
break
case 'hvac':
@@ -266,13 +272,13 @@ const getSensorValues = (sensor: SensorDevice) => {
type: 'setpoint',
label: 'Target Temperature',
value: (Math.random() * 6 + 18).toFixed(1),
unit: '°C'
unit: '°C',
})
values.push({
type: 'mode',
label: 'Operating Mode',
value: ['Heat', 'Cool', 'Auto', 'Fan'][Math.floor(Math.random() * 4)],
unit: ''
unit: '',
})
break
case 'security':
@@ -280,13 +286,13 @@ const getSensorValues = (sensor: SensorDevice) => {
type: 'status',
label: 'Security Status',
value: Math.random() > 0.8 ? 'Alert' : 'Normal',
unit: ''
unit: '',
})
values.push({
type: 'armed',
label: 'System Armed',
value: Math.random() > 0.5 ? 'Yes' : 'No',
unit: ''
unit: '',
})
break
default:
@@ -295,7 +301,7 @@ const getSensorValues = (sensor: SensorDevice) => {
type: 'status',
label: 'Device Status',
value: sensor.status === 'online' ? 'Active' : 'Inactive',
unit: ''
unit: '',
})
}
}
@@ -305,7 +311,7 @@ const getSensorValues = (sensor: SensorDevice) => {
type: 'uptime',
label: 'Uptime',
value: Math.floor(Math.random() * 30 + 1),
unit: 'days'
unit: 'days',
})
return values
@@ -345,7 +351,7 @@ const getSensorTypeIcon = (type: string) => {
humidity: '💧',
hvac: '❄️',
lighting: '💡',
security: '🔒'
security: '🔒',
}
return icons[type as keyof typeof icons] || '📱'
}
@@ -358,17 +364,21 @@ const getSensorTypeStyle = (type: string) => {
humidity: { bg: 'bg-blue-100', text: 'text-blue-700' },
hvac: { bg: 'bg-cyan-100', text: 'text-cyan-700' },
lighting: { bg: 'bg-amber-100', text: 'text-amber-700' },
security: { bg: 'bg-purple-100', text: 'text-purple-700' }
security: { bg: 'bg-purple-100', text: 'text-purple-700' },
}
return styles[type as keyof typeof styles] || { bg: 'bg-gray-100', text: 'text-gray-700' }
}
const getSensorStatusColor = (status: string) => {
switch (status) {
case 'online': return 'bg-green-500'
case 'offline': return 'bg-gray-400'
case 'error': return 'bg-red-500'
default: return 'bg-gray-400'
case 'online':
return 'bg-green-500'
case 'offline':
return 'bg-gray-400'
case 'error':
return 'bg-red-500'
default:
return 'bg-gray-400'
}
}
@@ -383,7 +393,7 @@ const formatTime = (timestamp: number) => {
const now = new Date()
const diffMs = now.getTime() - date.getTime()
const diffSecs = Math.floor(diffMs / 1000)
if (diffSecs < 60) {
return `${diffSecs}s ago`
} else if (diffSecs < 3600) {
@@ -394,4 +404,4 @@ const formatTime = (timestamp: number) => {
return date.toLocaleDateString()
}
}
</script>
</script>

View File

@@ -1,26 +1,25 @@
<template>
<div class="bg-white rounded-2xl shadow-sm p-4">
<h6 class="text-sm font-bold text-gray-500 mb-4">Room Overview</h6>
<div class="space-y-4">
<div v-if="roomsList.length === 0" class="text-center text-gray-500 py-8">
No room data available. Connect sensors to see room metrics.
</div>
<div v-for="room in roomsList" :key="room.room" class="border border-gray-100 rounded-lg p-3">
<!-- Room Header -->
<div class="flex items-center justify-between mb-3">
<h3 class="font-medium text-gray-900">{{ room.room }}</h3>
<div class="flex items-center gap-2">
<!-- CO2 Status Indicator -->
<div
class="w-3 h-3 rounded-full"
:class="getCO2StatusColor(room.co2!.status)"
></div>
<div class="w-3 h-3 rounded-full" :class="getCO2StatusColor(room.co2!.status)"></div>
<!-- Occupancy Indicator -->
<div class="flex items-center gap-1 text-xs text-gray-500">
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
<path
d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
/>
</svg>
<span class="capitalize">{{ room.occupancyEstimate }}</span>
</div>
@@ -32,15 +31,21 @@
<!-- Energy -->
<div class="bg-blue-50 rounded p-2">
<div class="text-blue-600 font-medium">Energy</div>
<div class="text-blue-900">{{ room.energy!.current.toFixed(2) }} {{ room.energy!.unit }}</div>
<div class="text-blue-900">
{{ room.energy!.current.toFixed(2) }} {{ room.energy!.unit }}
</div>
<div class="text-blue-600 text-xs">Total: {{ room.energy!.total.toFixed(2) }}</div>
</div>
<!-- CO2 -->
<div class="rounded p-2" :class="getCO2BackgroundColor(room.co2!.status)">
<div class="font-medium" :class="getCO2TextColor(room.co2!.status)">CO2</div>
<div :class="getCO2TextColor(room.co2!.status)">{{ Math.round(room.co2!.current) }} {{ room.co2!.unit }}</div>
<div class="text-xs" :class="getCO2TextColor(room.co2!.status)">{{ room.co2!.status.toUpperCase() }}</div>
<div :class="getCO2TextColor(room.co2!.status)">
{{ Math.round(room.co2!.current) }} {{ room.co2!.unit }}
</div>
<div class="text-xs" :class="getCO2TextColor(room.co2!.status)">
{{ room.co2!.status.toUpperCase() }}
</div>
</div>
</div>
@@ -53,7 +58,10 @@
</div>
<!-- Summary Stats -->
<div v-if="roomsList.length > 0" class="mt-4 pt-4 border-t border-gray-100 grid grid-cols-3 gap-4 text-center text-xs">
<div
v-if="roomsList.length > 0"
class="mt-4 pt-4 border-t border-gray-100 grid grid-cols-3 gap-4 text-center text-xs"
>
<div>
<div class="font-medium text-gray-900">{{ roomsList.length }}</div>
<div class="text-gray-500">Rooms</div>
@@ -78,7 +86,7 @@ const roomStore = useRoomStore()
const roomsList = computed(() => {
return Array.from(roomStore.roomsData.values())
.filter(room => room.energy && room.co2) // Only show rooms with both metrics
.filter((room) => room.energy && room.co2) // Only show rooms with both metrics
.sort((a, b) => a.room.localeCompare(b.room))
})
@@ -94,31 +102,46 @@ const averageCO2 = computed(() => {
const getCO2StatusColor = (status: string) => {
switch (status) {
case 'good': return 'bg-green-500'
case 'moderate': return 'bg-yellow-500'
case 'poor': return 'bg-orange-500'
case 'critical': return 'bg-red-500'
default: return 'bg-gray-500'
case 'good':
return 'bg-green-500'
case 'moderate':
return 'bg-yellow-500'
case 'poor':
return 'bg-orange-500'
case 'critical':
return 'bg-red-500'
default:
return 'bg-gray-500'
}
}
const getCO2BackgroundColor = (status: string) => {
switch (status) {
case 'good': return 'bg-green-50'
case 'moderate': return 'bg-yellow-50'
case 'poor': return 'bg-orange-50'
case 'critical': return 'bg-red-50'
default: return 'bg-gray-50'
case 'good':
return 'bg-green-50'
case 'moderate':
return 'bg-yellow-50'
case 'poor':
return 'bg-orange-50'
case 'critical':
return 'bg-red-50'
default:
return 'bg-gray-50'
}
}
const getCO2TextColor = (status: string) => {
switch (status) {
case 'good': return 'text-green-700'
case 'moderate': return 'text-yellow-700'
case 'poor': return 'text-orange-700'
case 'critical': return 'text-red-700'
default: return 'text-gray-700'
case 'good':
return 'text-green-700'
case 'moderate':
return 'text-yellow-700'
case 'poor':
return 'text-orange-700'
case 'critical':
return 'text-red-700'
default:
return 'text-gray-700'
}
}
@@ -127,7 +150,7 @@ const formatTime = (timestamp: number) => {
const now = new Date()
const diffMs = now.getTime() - date.getTime()
const diffSecs = Math.floor(diffMs / 1000)
if (diffSecs < 60) {
return `${diffSecs}s ago`
} else if (diffSecs < 3600) {
@@ -136,4 +159,4 @@ const formatTime = (timestamp: number) => {
return date.toLocaleTimeString()
}
}
</script>
</script>

View File

@@ -9,9 +9,7 @@
Token expires in: {{ formatTimeUntilExpiry() }}
</div>
<div v-if="authStore.error" class="auth-status__error">
Auth Error: {{ authStore.error }}
</div>
<div v-if="authStore.error" class="auth-status__error">Auth Error: {{ authStore.error }}</div>
<button
v-if="!authStore.isAuthenticated"
@@ -33,13 +31,13 @@ const authStore = useAuthStore()
const authStatusClass = computed(() => ({
'auth-status--authenticated': authStore.isAuthenticated,
'auth-status--error': !authStore.isAuthenticated || authStore.error,
'auth-status--loading': authStore.isLoading
'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
'auth-status__dot--yellow': authStore.isLoading,
}))
const statusText = computed(() => {
@@ -116,4 +114,4 @@ async function handleReauth() {
@apply bg-blue-500 text-white px-2 py-1 rounded text-xs hover:bg-blue-600 disabled:opacity-50;
}
}
</style>
</style>

View File

@@ -4,7 +4,9 @@
<div class="absolute inset-0 bg-black/50" @click="$emit('close')"></div>
<!-- Modal -->
<div class="relative bg-white rounded-2xl shadow-xl max-w-md w-full mx-4 max-h-[90vh] overflow-y-auto">
<div
class="relative bg-white rounded-2xl shadow-xl max-w-md w-full mx-4 max-h-[90vh] overflow-y-auto"
>
<!-- Header -->
<div class="p-6 border-b border-gray-100">
<div class="flex items-center justify-between">

View File

@@ -5,12 +5,17 @@
<div class="p-6 border-b border-gray-200">
<div class="flex items-center justify-between">
<h2 class="text-xl font-semibold text-gray-900">Room Management</h2>
<button
<button
@click="$emit('close')"
class="text-gray-400 hover:text-gray-600 transition-colors"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
@@ -53,49 +58,54 @@
</div>
<div class="grid grid-cols-1 gap-3">
<div
v-for="room in roomsWithStats"
:key="room.name"
class="bg-gray-50 rounded-lg p-4"
>
<div v-for="room in roomsWithStats" :key="room.name" class="bg-gray-50 rounded-lg p-4">
<div class="flex items-center justify-between">
<div class="flex-1">
<div class="flex items-center gap-3 mb-2">
<h4 class="font-medium text-gray-900">{{ room.name }}</h4>
<span class="text-xs px-2 py-1 bg-blue-100 text-blue-700 rounded-full font-medium">
<span
class="text-xs px-2 py-1 bg-blue-100 text-blue-700 rounded-full font-medium"
>
{{ room.sensorCount }} sensors
</span>
</div>
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 text-sm">
<div>
<span class="text-gray-600">Types:</span>
<div class="flex flex-wrap gap-1 mt-1">
<span
v-for="type in room.sensorTypes"
<span
v-for="type in room.sensorTypes"
:key="type"
class="text-xs px-2 py-0.5 bg-gray-200 text-gray-700 rounded"
>
{{ type }}
</span>
<span v-if="room.sensorTypes.length === 0" class="text-xs text-gray-500">None</span>
<span v-if="room.sensorTypes.length === 0" class="text-xs text-gray-500"
>None</span
>
</div>
</div>
<div>
<span class="text-gray-600">Energy:</span>
<div class="font-medium" :class="room.hasMetrics ? 'text-gray-900' : 'text-gray-400'">
{{ room.hasMetrics ? room.energyConsumption.toFixed(2) + ' kWh' : 'No data' }}
<div
class="font-medium"
:class="room.hasMetrics ? 'text-gray-900' : 'text-gray-400'"
>
{{
room.hasMetrics ? room.energyConsumption.toFixed(2) + ' kWh' : 'No data'
}}
</div>
</div>
<div>
<span class="text-gray-600">CO2:</span>
<div class="font-medium" :class="getCO2Color(room.co2Level)">
{{ room.hasMetrics ? Math.round(room.co2Level) + ' ppm' : 'No data' }}
</div>
</div>
<div>
<span class="text-gray-600">Last Update:</span>
<div class="text-xs text-gray-500">
@@ -104,7 +114,7 @@
</div>
</div>
</div>
<div class="ml-4">
<button
@click="confirmDeleteRoom(room.name)"
@@ -118,7 +128,7 @@
</div>
</div>
</div>
<!-- Empty state -->
<div v-if="roomsWithStats.length === 0" class="text-center py-8">
<div class="text-gray-400 text-4xl mb-2">🏢</div>
@@ -132,7 +142,7 @@
<!-- Footer -->
<div class="p-6 border-t border-gray-200 bg-gray-50">
<div class="flex justify-end gap-3">
<button
<button
@click="$emit('close')"
class="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg text-sm font-medium hover:bg-gray-50 transition-colors"
>
@@ -143,23 +153,28 @@
</div>
<!-- Delete Confirmation Modal -->
<div v-if="roomToDelete" class="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-60">
<div
v-if="roomToDelete"
class="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-60"
>
<div class="bg-white rounded-xl max-w-md w-full p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-2">Delete Room</h3>
<p class="text-gray-600 mb-4">
Are you sure you want to delete <strong>"{{ roomToDelete }}"</strong>?
{{ getRoomStats(roomToDelete).sensorCount > 0
? `This will unassign ${getRoomStats(roomToDelete).sensorCount} sensor(s).`
: 'This action cannot be undone.' }}
Are you sure you want to delete <strong>"{{ roomToDelete }}"</strong>?
{{
getRoomStats(roomToDelete).sensorCount > 0
? `This will unassign ${getRoomStats(roomToDelete).sensorCount} sensor(s).`
: 'This action cannot be undone.'
}}
</p>
<div class="flex gap-3">
<button
<button
@click="roomToDelete = null"
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
</button>
<button
<button
@click="deleteRoom"
class="flex-1 px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg text-sm font-medium transition-colors"
>
@@ -274,7 +289,7 @@ const formatTime = (timestamp: number) => {
const now = new Date()
const diffMs = now.getTime() - date.getTime()
const diffSecs = Math.floor(diffMs / 1000)
if (diffSecs < 60) {
return `${diffSecs}s ago`
} else if (diffSecs < 3600) {
@@ -296,4 +311,4 @@ const clearError = () => {
// Watch for changes in newRoomName to clear errors
import { watch } from 'vue'
watch(newRoomName, clearError)
</script>
</script>

View File

@@ -21,7 +21,7 @@ import {
type HealthCheck,
type SystemStatus,
type DataQuery,
type DataResponse
type DataResponse,
} from '@/services'
interface ApiState {
@@ -33,16 +33,16 @@ export function useApi() {
// Global API state
const globalState = reactive<ApiState>({
loading: false,
error: null
error: null,
})
// Helper to handle API calls with state management
async function handleApiCall<T>(
apiCall: () => Promise<T>,
localState?: { loading: boolean; error: string | null }
localState?: { loading: boolean; error: string | null },
): Promise<T | null> {
const state = localState || globalState
state.loading = true
state.error = null
@@ -61,7 +61,7 @@ export function useApi() {
return {
globalState,
handleApiCall
handleApiCall,
}
}
@@ -69,7 +69,7 @@ export function useApi() {
export function useSensorsApi() {
const state = reactive<ApiState>({
loading: false,
error: null
error: null,
})
const sensors = ref<SensorDevice[]>([])
@@ -83,10 +83,7 @@ export function useSensorsApi() {
sensor_type?: SensorType
status?: SensorStatus
}) => {
const result = await handleApiCall(
() => sensorsApi.getSensors(params),
state
)
const result = await handleApiCall(() => sensorsApi.getSensors(params), state)
if (result && result.sensors) {
sensors.value = result.sensors
}
@@ -94,10 +91,7 @@ export function useSensorsApi() {
}
const fetchSensor = async (sensorId: string) => {
const result = await handleApiCall(
() => sensorsApi.getSensor(sensorId),
state
)
const result = await handleApiCall(() => sensorsApi.getSensor(sensorId), state)
if (result) {
currentSensor.value = result
}
@@ -111,12 +105,9 @@ export function useSensorsApi() {
end_time?: number
limit?: number
offset?: number
}
},
) => {
const result = await handleApiCall(
() => sensorsApi.getSensorData(sensorId, params),
state
)
const result = await handleApiCall(() => sensorsApi.getSensorData(sensorId, params), state)
if (result) {
sensorData.value = result
}
@@ -124,27 +115,15 @@ export function useSensorsApi() {
}
const queryData = async (query: DataQuery) => {
return handleApiCall(
() => sensorsApi.queryData(query),
state
)
return handleApiCall(() => sensorsApi.queryData(query), state)
}
const updateSensorMetadata = async (
sensorId: string,
metadata: SensorMetadata
) => {
return handleApiCall(
() => sensorsApi.updateSensorMetadata(sensorId, metadata),
state
)
const updateSensorMetadata = async (sensorId: string, metadata: SensorMetadata) => {
return handleApiCall(() => sensorsApi.updateSensorMetadata(sensorId, metadata), state)
}
const deleteSensor = async (sensorId: string) => {
return handleApiCall(
() => sensorsApi.deleteSensor(sensorId),
state
)
return handleApiCall(() => sensorsApi.deleteSensor(sensorId), state)
}
const exportData = async (params: {
@@ -153,10 +132,7 @@ export function useSensorsApi() {
sensor_ids?: string
format?: 'json' | 'csv'
}) => {
return handleApiCall(
() => sensorsApi.exportData(params),
state
)
return handleApiCall(() => sensorsApi.exportData(params), state)
}
return {
@@ -170,7 +146,7 @@ export function useSensorsApi() {
queryData,
updateSensorMetadata,
deleteSensor,
exportData
exportData,
}
}
@@ -178,7 +154,7 @@ export function useSensorsApi() {
export function useRoomsApi() {
const state = reactive<ApiState>({
loading: false,
error: null
error: null,
})
const rooms = ref<RoomInfo[]>([])
@@ -187,10 +163,7 @@ export function useRoomsApi() {
const { handleApiCall } = useApi()
const fetchRooms = async () => {
const result = await handleApiCall(
() => roomsApi.getRooms(),
state
)
const result = await handleApiCall(() => roomsApi.getRooms(), state)
if (result) {
rooms.value = result
}
@@ -203,12 +176,9 @@ export function useRoomsApi() {
start_time?: number
end_time?: number
limit?: number
}
},
) => {
const result = await handleApiCall(
() => roomsApi.getRoomData(roomName, params),
state
)
const result = await handleApiCall(() => roomsApi.getRoomData(roomName, params), state)
if (result) {
currentRoomData.value = result
}
@@ -220,7 +190,7 @@ export function useRoomsApi() {
rooms,
currentRoomData,
fetchRooms,
fetchRoomData
fetchRoomData,
}
}
@@ -228,7 +198,7 @@ export function useRoomsApi() {
export function useAnalyticsApi() {
const state = reactive<ApiState>({
loading: false,
error: null
error: null,
})
const summary = ref<AnalyticsSummary | null>(null)
@@ -239,10 +209,7 @@ export function useAnalyticsApi() {
const { handleApiCall } = useApi()
const fetchAnalyticsSummary = async (hours: number = 24) => {
const result = await handleApiCall(
() => analyticsApi.getAnalyticsSummary(hours),
state
)
const result = await handleApiCall(() => analyticsApi.getAnalyticsSummary(hours), state)
if (result) {
summary.value = result
}
@@ -250,10 +217,7 @@ export function useAnalyticsApi() {
}
const fetchEnergyTrends = async (hours: number = 168) => {
const result = await handleApiCall(
() => analyticsApi.getEnergyTrends(hours),
state
)
const result = await handleApiCall(() => analyticsApi.getEnergyTrends(hours), state)
if (result) {
trends.value = result
}
@@ -261,10 +225,7 @@ export function useAnalyticsApi() {
}
const fetchRoomComparison = async (hours: number = 24) => {
const result = await handleApiCall(
() => analyticsApi.getRoomComparison(hours),
state
)
const result = await handleApiCall(() => analyticsApi.getRoomComparison(hours), state)
if (result) {
roomComparison.value = result
}
@@ -277,10 +238,7 @@ export function useAnalyticsApi() {
hours?: number
limit?: number
}) => {
const result = await handleApiCall(
() => analyticsApi.getEvents(params),
state
)
const result = await handleApiCall(() => analyticsApi.getEvents(params), state)
if (result) {
events.value = result.events
}
@@ -296,7 +254,7 @@ export function useAnalyticsApi() {
fetchAnalyticsSummary,
fetchEnergyTrends,
fetchRoomComparison,
fetchEvents
fetchEvents,
}
}
@@ -304,7 +262,7 @@ export function useAnalyticsApi() {
export function useHealthApi() {
const state = reactive<ApiState>({
loading: false,
error: null
error: null,
})
const health = ref<HealthCheck | null>(null)
@@ -313,10 +271,7 @@ export function useHealthApi() {
const { handleApiCall } = useApi()
const fetchHealth = async () => {
const result = await handleApiCall(
() => healthApi.getHealth(),
state
)
const result = await handleApiCall(() => healthApi.getHealth(), state)
if (result) {
health.value = result
}
@@ -324,10 +279,7 @@ export function useHealthApi() {
}
const fetchStatus = async () => {
const result = await handleApiCall(
() => healthApi.getStatus(),
state
)
const result = await handleApiCall(() => healthApi.getStatus(), state)
if (result) {
status.value = result
}
@@ -339,6 +291,6 @@ export function useHealthApi() {
health,
status,
fetchHealth,
fetchStatus
fetchStatus,
}
}
}

View File

@@ -346,7 +346,10 @@ class ApiClient {
}
}
async get<T>(endpoint: string, params?: Record<string, string | number | boolean | string[]>): Promise<T> {
async get<T>(
endpoint: string,
params?: Record<string, string | number | boolean | string[]>,
): Promise<T> {
const url = new URL(`${this.baseUrl}${endpoint}`)
if (params) {

View File

@@ -25,7 +25,10 @@ export const authApi = {
},
async saveToken(token: string): Promise<{ token: string; datetime: string; active: boolean }> {
return apiClient.post<{ token: string; datetime: string; active: boolean }>('/api/v1/tokens/save', { token })
return apiClient.post<{ token: string; datetime: string; active: boolean }>(
'/api/v1/tokens/save',
{ token },
)
},
async validateToken(token: string): Promise<TokenValidation> {

View File

@@ -26,5 +26,5 @@ export type {
RoomComparison,
SystemEvent,
HealthCheck,
SystemStatus
} from './api'
SystemStatus,
} from './api'

View File

@@ -131,11 +131,7 @@ export const useAnalyticsStore = defineStore('analytics', () => {
// Initialize data from APIs
async function initializeAnalyticsFromApi() {
await Promise.allSettled([
fetchAnalyticsSummary(),
fetchSystemStatus(),
fetchHealthStatus(),
])
await Promise.allSettled([fetchAnalyticsSummary(), fetchSystemStatus(), fetchHealthStatus()])
}
return {
@@ -155,4 +151,4 @@ export const useAnalyticsStore = defineStore('analytics', () => {
fetchHealthStatus,
initializeAnalyticsFromApi,
}
})
})

View File

@@ -199,7 +199,7 @@ export const useAuthStore = defineStore('auth', () => {
}
// Initialize on store creation (async)
loadTokenFromStorage().catch(error => {
loadTokenFromStorage().catch((error) => {
console.warn('Failed to load token from storage:', error)
})

View File

@@ -13,4 +13,4 @@ export type {
RoomComparison,
SystemStatus,
HealthCheck,
} from '@/services'
} from '@/services'

View File

@@ -316,7 +316,9 @@ export const useRoomStore = defineStore('room', () => {
apiRooms.value = roomsArray
// Update available rooms from API data
const roomNames = roomsArray.map((room: any) => room.name || room.room).filter((name: string) => name)
const roomNames = roomsArray
.map((room: any) => room.name || room.room)
.filter((name: string) => name)
if (roomNames.length > 0) {
availableRooms.value = [...new Set([...availableRooms.value, ...roomNames])].sort()
}

View File

@@ -38,7 +38,7 @@ export const useSensorStore = defineStore('sensor', () => {
// Aggregated CO2 metrics
const averageCO2Level = computed<number>(() => {
const readings = Array.from(latestReadings.values())
const co2Readings = readings.filter(r => r.co2?.value !== undefined)
const co2Readings = readings.filter((r) => r.co2?.value !== undefined)
if (co2Readings.length === 0) return 0
@@ -49,8 +49,8 @@ export const useSensorStore = defineStore('sensor', () => {
const maxCO2Level = computed<number>(() => {
const readings = Array.from(latestReadings.values())
const co2Values = readings
.filter(r => r.co2?.value !== undefined)
.map(r => r.co2?.value || 0)
.filter((r) => r.co2?.value !== undefined)
.map((r) => r.co2?.value || 0)
return co2Values.length > 0 ? Math.max(...co2Values) : 0
})

View File

@@ -117,7 +117,10 @@ export const useWebSocketStore = defineStore('websocket', () => {
function processIncomingData(data: WebSocketReading): void {
// Skip non-data messages
if ('type' in data && (data as any).type === 'connection_established' || (data as any).type === 'proxy_info') {
if (
('type' in data && (data as any).type === 'connection_established') ||
(data as any).type === 'proxy_info'
) {
return
}

View File

@@ -220,11 +220,7 @@
No rooms found from API
</div>
<div v-else class="space-y-3">
<div
v-for="room in apiRooms"
:key="room.room"
class="p-3 bg-gray-50 rounded-lg"
>
<div v-for="room in apiRooms" :key="room.room" class="p-3 bg-gray-50 rounded-lg">
<div class="flex items-center justify-between mb-2">
<p class="font-medium text-gray-900">{{ room.room }}</p>
<span class="text-sm text-gray-500">{{ room.sensor_count }} sensors</span>

View File

@@ -257,7 +257,11 @@ interface ActionParameters {
[key: string]: unknown
}
const handleActionExecute = async (sensorId: string, actionId: string, parameters: ActionParameters) => {
const handleActionExecute = async (
sensorId: string,
actionId: string,
parameters: ActionParameters,
) => {
isExecutingAction.value = true
try {
await sensorStore.executeSensorAction(sensorId, actionId)

View File

@@ -28,9 +28,11 @@
<button
@click="activeSection = section.id"
class="w-full flex items-center gap-3 px-3 py-2 rounded-lg text-left transition-colors"
:class="activeSection === section.id
? 'bg-blue-100 text-blue-700'
: 'text-gray-700 hover:bg-gray-100'"
:class="
activeSection === section.id
? 'bg-blue-100 text-blue-700'
: 'text-gray-700 hover:bg-gray-100'
"
>
<span class="text-lg">{{ section.icon }}</span>
<div>
@@ -46,7 +48,10 @@
<!-- Settings Content -->
<div class="lg:col-span-2 space-y-6">
<!-- Appearance Settings -->
<div v-if="activeSection === 'appearance'" class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div
v-if="activeSection === 'appearance'"
class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden"
>
<div class="p-6 border-b border-gray-100">
<h3 class="text-lg font-semibold text-gray-900">Appearance</h3>
<p class="text-gray-600 text-sm mt-1">Customize the look and feel of your dashboard</p>
@@ -61,9 +66,11 @@
:key="theme.value"
@click="settingsStore.updateSetting('theme', theme.value)"
class="relative p-4 border-2 rounded-lg cursor-pointer transition-all hover:border-blue-300"
:class="settingsStore.settings.theme === theme.value
? 'border-blue-500 bg-blue-50'
: 'border-gray-200'"
:class="
settingsStore.settings.theme === theme.value
? 'border-blue-500 bg-blue-50'
: 'border-gray-200'
"
>
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
@@ -72,7 +79,11 @@
</div>
<div v-if="settingsStore.settings.theme === theme.value" class="text-blue-600">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
<path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd"
/>
</svg>
</div>
</div>
@@ -89,9 +100,11 @@
:key="mode.value"
@click="settingsStore.updateSetting('ui.navigationMode', mode.value)"
class="relative p-4 border-2 rounded-lg cursor-pointer transition-all hover:border-blue-300"
:class="settingsStore.settings.ui.navigationMode === mode.value
? 'border-blue-500 bg-blue-50'
: 'border-gray-200'"
:class="
settingsStore.settings.ui.navigationMode === mode.value
? 'border-blue-500 bg-blue-50'
: 'border-gray-200'
"
>
<div class="flex items-start justify-between">
<div class="flex items-start gap-3">
@@ -101,9 +114,16 @@
<div class="text-sm text-gray-600 mt-1">{{ mode.description }}</div>
</div>
</div>
<div v-if="settingsStore.settings.ui.navigationMode === mode.value" class="text-blue-600 mt-1">
<div
v-if="settingsStore.settings.ui.navigationMode === mode.value"
class="text-blue-600 mt-1"
>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
<path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd"
/>
</svg>
</div>
</div>
@@ -119,12 +139,21 @@
<div class="text-sm text-gray-600">Reduce spacing and padding</div>
</div>
<button
@click="settingsStore.updateSetting('ui.compactMode', !settingsStore.settings.ui.compactMode)"
@click="
settingsStore.updateSetting(
'ui.compactMode',
!settingsStore.settings.ui.compactMode,
)
"
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
:class="settingsStore.settings.ui.compactMode ? 'bg-blue-600' : 'bg-gray-200'"
>
<span class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
:class="settingsStore.settings.ui.compactMode ? 'translate-x-6' : 'translate-x-1'"></span>
<span
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
:class="
settingsStore.settings.ui.compactMode ? 'translate-x-6' : 'translate-x-1'
"
></span>
</button>
</div>
@@ -134,12 +163,21 @@
<div class="text-sm text-gray-600">Enable smooth transitions</div>
</div>
<button
@click="settingsStore.updateSetting('ui.showAnimations', !settingsStore.settings.ui.showAnimations)"
@click="
settingsStore.updateSetting(
'ui.showAnimations',
!settingsStore.settings.ui.showAnimations,
)
"
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
:class="settingsStore.settings.ui.showAnimations ? 'bg-blue-600' : 'bg-gray-200'"
>
<span class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
:class="settingsStore.settings.ui.showAnimations ? 'translate-x-6' : 'translate-x-1'"></span>
<span
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
:class="
settingsStore.settings.ui.showAnimations ? 'translate-x-6' : 'translate-x-1'
"
></span>
</button>
</div>
</div>
@@ -147,7 +185,10 @@
</div>
<!-- Data & Sync Settings -->
<div v-if="activeSection === 'data'" class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div
v-if="activeSection === 'data'"
class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden"
>
<div class="p-6 border-b border-gray-100">
<h3 class="text-lg font-semibold text-gray-900">Data & Synchronization</h3>
<p class="text-gray-600 text-sm mt-1">Configure data refresh and connection settings</p>
@@ -160,12 +201,19 @@
<div class="text-sm text-gray-600">Automatically refresh data periodically</div>
</div>
<button
@click="settingsStore.updateSetting('ui.autoRefresh', !settingsStore.settings.ui.autoRefresh)"
@click="
settingsStore.updateSetting(
'ui.autoRefresh',
!settingsStore.settings.ui.autoRefresh,
)
"
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
:class="settingsStore.settings.ui.autoRefresh ? 'bg-blue-600' : 'bg-gray-200'"
>
<span class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
:class="settingsStore.settings.ui.autoRefresh ? 'translate-x-6' : 'translate-x-1'"></span>
<span
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
:class="settingsStore.settings.ui.autoRefresh ? 'translate-x-6' : 'translate-x-1'"
></span>
</button>
</div>
@@ -178,10 +226,17 @@
min="1"
max="60"
:value="settingsStore.settings.ui.refreshInterval"
@input="settingsStore.updateSetting('ui.refreshInterval', parseInt(($event.target as HTMLInputElement).value))"
@input="
settingsStore.updateSetting(
'ui.refreshInterval',
parseInt(($event.target as HTMLInputElement).value),
)
"
class="flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"
/>
<div class="bg-gray-100 px-3 py-1 rounded text-sm font-medium min-w-[80px] text-center">
<div
class="bg-gray-100 px-3 py-1 rounded text-sm font-medium min-w-[80px] text-center"
>
{{ settingsStore.settings.ui.refreshInterval }}s
</div>
</div>
@@ -196,43 +251,62 @@
type="text"
placeholder="ws://localhost:8000/ws"
class="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
:class="{ 'border-red-300 focus:ring-red-500 focus:border-red-500': websocketUrlError }"
:class="{
'border-red-300 focus:ring-red-500 focus:border-red-500': websocketUrlError,
}"
/>
<button
@click="updateWebSocketUrl"
:disabled="!websocketUrlInput || websocketUrlInput === settingsStore.settings.websocketUrl"
:disabled="
!websocketUrlInput || websocketUrlInput === settingsStore.settings.websocketUrl
"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-300 text-white rounded-lg text-sm font-medium transition-colors"
>
Update
</button>
</div>
<p v-if="websocketUrlError" class="text-red-600 text-sm mt-1">{{ websocketUrlError }}</p>
<p v-else class="text-gray-500 text-sm mt-1">Current: {{ settingsStore.settings.websocketUrl }}</p>
<p v-if="websocketUrlError" class="text-red-600 text-sm mt-1">
{{ websocketUrlError }}
</p>
<p v-else class="text-gray-500 text-sm mt-1">
Current: {{ settingsStore.settings.websocketUrl }}
</p>
</div>
<!-- Auto Connect -->
<div class="flex items-center justify-between p-4 border border-gray-200 rounded-lg">
<div>
<div class="font-medium text-gray-900">Auto Connect</div>
<div class="text-sm text-gray-600">Automatically connect to WebSocket on app start</div>
<div class="text-sm text-gray-600">
Automatically connect to WebSocket on app start
</div>
</div>
<button
@click="settingsStore.updateSetting('autoConnect', !settingsStore.settings.autoConnect)"
@click="
settingsStore.updateSetting('autoConnect', !settingsStore.settings.autoConnect)
"
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
:class="settingsStore.settings.autoConnect ? 'bg-blue-600' : 'bg-gray-200'"
>
<span class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
:class="settingsStore.settings.autoConnect ? 'translate-x-6' : 'translate-x-1'"></span>
<span
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
:class="settingsStore.settings.autoConnect ? 'translate-x-6' : 'translate-x-1'"
></span>
</button>
</div>
</div>
</div>
<!-- Notifications Settings -->
<div v-if="activeSection === 'notifications'" class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div
v-if="activeSection === 'notifications'"
class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden"
>
<div class="p-6 border-b border-gray-100">
<h3 class="text-lg font-semibold text-gray-900">Notifications</h3>
<p class="text-gray-600 text-sm mt-1">Configure how you receive alerts and notifications</p>
<p class="text-gray-600 text-sm mt-1">
Configure how you receive alerts and notifications
</p>
</div>
<div class="p-6 space-y-6">
<!-- Enable Notifications -->
@@ -242,34 +316,62 @@
<div class="text-sm text-gray-600">Receive system alerts and updates</div>
</div>
<button
@click="settingsStore.updateSetting('notifications.enabled', !settingsStore.settings.notifications.enabled)"
@click="
settingsStore.updateSetting(
'notifications.enabled',
!settingsStore.settings.notifications.enabled,
)
"
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
:class="settingsStore.settings.notifications.enabled ? 'bg-blue-600' : 'bg-gray-200'"
:class="
settingsStore.settings.notifications.enabled ? 'bg-blue-600' : 'bg-gray-200'
"
>
<span class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
:class="settingsStore.settings.notifications.enabled ? 'translate-x-6' : 'translate-x-1'"></span>
<span
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
:class="
settingsStore.settings.notifications.enabled ? 'translate-x-6' : 'translate-x-1'
"
></span>
</button>
</div>
<div v-if="settingsStore.settings.notifications.enabled" class="space-y-4">
<!-- Notification Types -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="flex items-center justify-between p-4 border border-gray-200 rounded-lg">
<div
class="flex items-center justify-between p-4 border border-gray-200 rounded-lg"
>
<div>
<div class="font-medium text-gray-900">Sound Alerts</div>
<div class="text-sm text-gray-600">Play sound for notifications</div>
</div>
<button
@click="settingsStore.updateSetting('notifications.sound', !settingsStore.settings.notifications.sound)"
@click="
settingsStore.updateSetting(
'notifications.sound',
!settingsStore.settings.notifications.sound,
)
"
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
:class="settingsStore.settings.notifications.sound ? 'bg-blue-600' : 'bg-gray-200'"
:class="
settingsStore.settings.notifications.sound ? 'bg-blue-600' : 'bg-gray-200'
"
>
<span class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
:class="settingsStore.settings.notifications.sound ? 'translate-x-6' : 'translate-x-1'"></span>
<span
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
:class="
settingsStore.settings.notifications.sound
? 'translate-x-6'
: 'translate-x-1'
"
></span>
</button>
</div>
<div class="flex items-center justify-between p-4 border border-gray-200 rounded-lg">
<div
class="flex items-center justify-between p-4 border border-gray-200 rounded-lg"
>
<div>
<div class="font-medium text-gray-900">Desktop Notifications</div>
<div class="text-sm text-gray-600">Show browser notifications</div>
@@ -277,10 +379,18 @@
<button
@click="toggleDesktopNotifications"
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
:class="settingsStore.settings.notifications.desktop ? 'bg-blue-600' : 'bg-gray-200'"
:class="
settingsStore.settings.notifications.desktop ? 'bg-blue-600' : 'bg-gray-200'
"
>
<span class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
:class="settingsStore.settings.notifications.desktop ? 'translate-x-6' : 'translate-x-1'"></span>
<span
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
:class="
settingsStore.settings.notifications.desktop
? 'translate-x-6'
: 'translate-x-1'
"
></span>
</button>
</div>
</div>
@@ -292,12 +402,27 @@
<div class="text-sm text-gray-600">Only show high-priority notifications</div>
</div>
<button
@click="settingsStore.updateSetting('notifications.criticalOnly', !settingsStore.settings.notifications.criticalOnly)"
@click="
settingsStore.updateSetting(
'notifications.criticalOnly',
!settingsStore.settings.notifications.criticalOnly,
)
"
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
:class="settingsStore.settings.notifications.criticalOnly ? 'bg-blue-600' : 'bg-gray-200'"
:class="
settingsStore.settings.notifications.criticalOnly
? 'bg-blue-600'
: 'bg-gray-200'
"
>
<span class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
:class="settingsStore.settings.notifications.criticalOnly ? 'translate-x-6' : 'translate-x-1'"></span>
<span
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
:class="
settingsStore.settings.notifications.criticalOnly
? 'translate-x-6'
: 'translate-x-1'
"
></span>
</button>
</div>
</div>
@@ -305,7 +430,10 @@
</div>
<!-- Advanced Settings -->
<div v-if="activeSection === 'advanced'" class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div
v-if="activeSection === 'advanced'"
class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden"
>
<div class="p-6 border-b border-gray-100">
<h3 class="text-lg font-semibold text-gray-900">Advanced</h3>
<p class="text-gray-600 text-sm mt-1">Developer options and advanced configuration</p>
@@ -318,12 +446,19 @@
<div class="text-sm text-gray-600">Enable debug logs and development features</div>
</div>
<button
@click="settingsStore.updateSetting('developerMode', !settingsStore.settings.developerMode)"
@click="
settingsStore.updateSetting(
'developerMode',
!settingsStore.settings.developerMode,
)
"
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
:class="settingsStore.settings.developerMode ? 'bg-blue-600' : 'bg-gray-200'"
>
<span class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
:class="settingsStore.settings.developerMode ? 'translate-x-6' : 'translate-x-1'"></span>
<span
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
:class="settingsStore.settings.developerMode ? 'translate-x-6' : 'translate-x-1'"
></span>
</button>
</div>
@@ -357,7 +492,9 @@
</button>
</div>
<p v-if="importError" class="text-red-600 text-sm mt-1">{{ importError }}</p>
<p v-if="importSuccess" class="text-green-600 text-sm mt-1">Settings imported successfully!</p>
<p v-if="importSuccess" class="text-green-600 text-sm mt-1">
Settings imported successfully!
</p>
</div>
</div>
</div>
@@ -366,20 +503,24 @@
</div>
<!-- Reset Confirmation Dialog -->
<div v-if="showResetDialog" class="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
<div
v-if="showResetDialog"
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-2">Reset All Settings</h3>
<p class="text-gray-600 mb-4">
Are you sure you want to reset all settings to their default values? This action cannot be undone.
Are you sure you want to reset all settings to their default values? This action cannot be
undone.
</p>
<div class="flex gap-3">
<button
<button
@click="showResetDialog = 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"
>
Cancel
</button>
<button
<button
@click="resetAllSettings"
class="flex-1 px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg text-sm font-medium transition-colors"
>
@@ -412,26 +553,26 @@ const settingSections = [
id: 'appearance',
name: 'Appearance',
description: 'Theme & UI',
icon: '🎨'
icon: '🎨',
},
{
id: 'data',
name: 'Data & Sync',
description: 'Connection & refresh',
icon: '🔄'
icon: '🔄',
},
{
id: 'notifications',
name: 'Notifications',
description: 'Alerts & sounds',
icon: '🔔'
icon: '🔔',
},
{
id: 'advanced',
name: 'Advanced',
description: 'Developer options',
icon: '⚙️'
}
icon: '⚙️',
},
]
// Methods
@@ -439,7 +580,7 @@ const formatTime = (date: Date) => {
return new Intl.DateTimeFormat('en-US', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
second: '2-digit',
}).format(date)
}
@@ -448,7 +589,7 @@ const updateWebSocketUrl = () => {
websocketUrlError.value = 'Please enter a valid WebSocket URL (ws:// or wss://)'
return
}
settingsStore.updateSetting('websocketUrl', websocketUrlInput.value)
websocketUrlError.value = ''
}
@@ -470,7 +611,7 @@ const exportSettings = () => {
const settingsJson = settingsStore.exportSettings()
const blob = new Blob([settingsJson], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `dashboard-settings-${new Date().toISOString().split('T')[0]}.json`
@@ -483,9 +624,9 @@ const exportSettings = () => {
const importSettings = () => {
importError.value = ''
importSuccess.value = false
const success = settingsStore.importSettings(importSettingsJson.value)
if (success) {
importSuccess.value = true
importSettingsJson.value = ''
@@ -508,4 +649,4 @@ onMounted(() => {
settingsStore.initialize()
websocketUrlInput.value = settingsStore.settings.websocketUrl
})
</script>
</script>