WellNuo/components/sensors/SensorHealthCard.tsx
Sergei 3f0fe56e02 Add protected route middleware and auth store for web app
- Implement Next.js middleware for route protection
- Create Zustand auth store for web (similar to mobile)
- Add comprehensive tests for middleware and auth store
- Protect authenticated routes (/dashboard, /profile)
- Redirect unauthenticated users to /login
- Redirect authenticated users from auth routes to /dashboard
- Handle session expiration with 401 callback
- Set access token cookie for middleware
- All tests passing (105 tests total)
2026-01-31 17:49:21 -08:00

262 lines
7.5 KiB
TypeScript

import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import {
SensorHealthMetrics,
SensorHealthStatus,
WiFiSignalQuality,
} from '@/services/ble/types';
import { AppColors, FontSizes, FontWeights, Spacing, BorderRadius } from '@/constants/theme';
interface SensorHealthCardProps {
metrics: SensorHealthMetrics;
}
export function SensorHealthCard({ metrics }: SensorHealthCardProps) {
const getHealthColor = (status: SensorHealthStatus): string => {
switch (status) {
case SensorHealthStatus.EXCELLENT:
return AppColors.success;
case SensorHealthStatus.GOOD:
return '#4CAF50';
case SensorHealthStatus.FAIR:
return AppColors.warning;
case SensorHealthStatus.POOR:
return '#FF9800';
case SensorHealthStatus.CRITICAL:
return AppColors.error;
default:
return AppColors.textSecondary;
}
};
const getHealthIcon = (status: SensorHealthStatus): keyof typeof Ionicons.glyphMap => {
switch (status) {
case SensorHealthStatus.EXCELLENT:
return 'checkmark-circle';
case SensorHealthStatus.GOOD:
return 'checkmark-circle-outline';
case SensorHealthStatus.FAIR:
return 'alert-circle-outline';
case SensorHealthStatus.POOR:
return 'warning-outline';
case SensorHealthStatus.CRITICAL:
return 'close-circle';
default:
return 'help-circle-outline';
}
};
const getWiFiSignalIcon = (quality: WiFiSignalQuality): keyof typeof Ionicons.glyphMap => {
switch (quality) {
case WiFiSignalQuality.EXCELLENT:
return 'wifi';
case WiFiSignalQuality.GOOD:
return 'wifi';
case WiFiSignalQuality.FAIR:
return 'wifi-outline';
case WiFiSignalQuality.WEAK:
return 'wifi-outline';
default:
return 'help-circle-outline';
}
};
const getWiFiSignalColor = (quality: WiFiSignalQuality): string => {
switch (quality) {
case WiFiSignalQuality.EXCELLENT:
return AppColors.success;
case WiFiSignalQuality.GOOD:
return '#4CAF50';
case WiFiSignalQuality.FAIR:
return AppColors.warning;
case WiFiSignalQuality.WEAK:
return AppColors.error;
default:
return AppColors.textSecondary;
}
};
const successRate =
metrics.communication.successfulCommands + metrics.communication.failedCommands > 0
? (
(metrics.communication.successfulCommands /
(metrics.communication.successfulCommands + metrics.communication.failedCommands)) *
100
).toFixed(1)
: '100';
return (
<View style={styles.container}>
{/* Header with Overall Health */}
<View style={styles.header}>
<View style={styles.healthIndicator}>
<Ionicons
name={getHealthIcon(metrics.overallHealth)}
size={24}
color={getHealthColor(metrics.overallHealth)}
/>
<Text style={[styles.healthText, { color: getHealthColor(metrics.overallHealth) }]}>
{metrics.overallHealth.charAt(0).toUpperCase() + metrics.overallHealth.slice(1)}
</Text>
</View>
<Text style={styles.deviceName}>{metrics.deviceName}</Text>
</View>
{/* Metrics Grid */}
<View style={styles.metricsGrid}>
{/* Connection Status */}
<View style={styles.metricItem}>
<Text style={styles.metricLabel}>Status</Text>
<View style={styles.metricValue}>
<View
style={[
styles.statusDot,
{
backgroundColor:
metrics.connectionStatus === 'online'
? AppColors.success
: metrics.connectionStatus === 'warning'
? AppColors.warning
: AppColors.error,
},
]}
/>
<Text style={styles.metricText}>
{metrics.connectionStatus.charAt(0).toUpperCase() + metrics.connectionStatus.slice(1)}
</Text>
</View>
</View>
{/* WiFi Signal */}
<View style={styles.metricItem}>
<Text style={styles.metricLabel}>WiFi Signal</Text>
<View style={styles.metricValue}>
<Ionicons
name={getWiFiSignalIcon(metrics.wifiSignalQuality)}
size={16}
color={getWiFiSignalColor(metrics.wifiSignalQuality)}
/>
<Text style={styles.metricText}>
{metrics.wifiRssi ? `${metrics.wifiRssi} dBm` : 'Unknown'}
</Text>
</View>
</View>
{/* Last Seen */}
<View style={styles.metricItem}>
<Text style={styles.metricLabel}>Last Seen</Text>
<Text style={styles.metricText}>{metrics.lastSeenMinutesAgo} min ago</Text>
</View>
{/* Success Rate */}
<View style={styles.metricItem}>
<Text style={styles.metricLabel}>Success Rate</Text>
<Text style={styles.metricText}>{successRate}%</Text>
</View>
</View>
{/* WiFi Network Info */}
{metrics.wifiSsid && (
<View style={styles.wifiInfo}>
<Ionicons name="wifi" size={16} color={AppColors.textSecondary} />
<Text style={styles.wifiText}>
Connected to {metrics.wifiSsid}
</Text>
</View>
)}
{/* Communication Stats */}
<View style={styles.commStats}>
<Text style={styles.commStatsLabel}>Communication</Text>
<Text style={styles.commStatsText}>
{metrics.communication.successfulCommands} successful · {' '}
{metrics.communication.failedCommands} failed · Avg {metrics.communication.averageResponseTime}ms
</Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
backgroundColor: AppColors.surface,
borderRadius: BorderRadius.md,
padding: Spacing.md,
marginBottom: Spacing.md,
},
header: {
marginBottom: Spacing.md,
},
healthIndicator: {
flexDirection: 'row',
alignItems: 'center',
gap: Spacing.sm,
marginBottom: Spacing.sm,
},
healthText: {
fontSize: FontSizes.base,
fontWeight: FontWeights.semibold,
},
deviceName: {
fontSize: FontSizes.sm,
color: AppColors.textSecondary,
},
metricsGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: Spacing.md,
marginBottom: Spacing.md,
},
metricItem: {
width: '48%',
},
metricLabel: {
fontSize: FontSizes.xs,
color: AppColors.textSecondary,
marginBottom: 4,
},
metricValue: {
flexDirection: 'row',
alignItems: 'center',
gap: Spacing.xs,
},
metricText: {
fontSize: FontSizes.sm,
fontWeight: FontWeights.medium,
color: AppColors.textPrimary,
},
statusDot: {
width: 8,
height: 8,
borderRadius: 4,
},
wifiInfo: {
flexDirection: 'row',
alignItems: 'center',
gap: Spacing.xs,
paddingTop: Spacing.sm,
borderTopWidth: 1,
borderTopColor: AppColors.border,
marginBottom: Spacing.sm,
},
wifiText: {
fontSize: FontSizes.xs,
color: AppColors.textSecondary,
},
commStats: {
paddingTop: Spacing.sm,
borderTopWidth: 1,
borderTopColor: AppColors.border,
},
commStatsLabel: {
fontSize: FontSizes.xs,
color: AppColors.textSecondary,
marginBottom: 4,
},
commStatsText: {
fontSize: FontSizes.xs,
color: AppColors.textSecondary,
},
});