/** * Online Status Service * * Provides functionality to check and monitor device/sensor online status. * Supports individual status checks and batch monitoring with configurable thresholds. */ import { api } from './api'; import type { WPSensor } from '@/types'; /** * Online status levels * - online: Device is actively communicating (< 5 minutes since last seen) * - warning: Device may have issues (5-60 minutes since last seen) * - offline: Device is not communicating (> 60 minutes since last seen) */ export type OnlineStatus = 'online' | 'warning' | 'offline'; /** * Thresholds for determining online status (in minutes) */ export interface StatusThresholds { /** Maximum minutes for 'online' status (default: 5) */ onlineMaxMinutes: number; /** Maximum minutes for 'warning' status (default: 60) */ warningMaxMinutes: number; } export const DEFAULT_THRESHOLDS: StatusThresholds = { onlineMaxMinutes: 5, warningMaxMinutes: 60, }; /** * Device status information */ export interface DeviceStatusInfo { deviceId: string; deviceName: string; status: OnlineStatus; lastSeen: Date; lastSeenMinutesAgo: number; lastSeenFormatted: string; wellId?: number; mac?: string; } /** * Beneficiary online status summary */ export interface BeneficiaryOnlineStatus { beneficiaryId: string; totalDevices: number; onlineCount: number; warningCount: number; offlineCount: number; overallStatus: OnlineStatus; devices: DeviceStatusInfo[]; lastChecked: Date; } /** * Status check result */ export interface StatusCheckResult { ok: boolean; data?: BeneficiaryOnlineStatus; error?: string; } /** * Calculate minutes since a given date */ function getMinutesAgo(date: Date): number { const now = new Date(); return Math.floor((now.getTime() - date.getTime()) / (1000 * 60)); } /** * Determine online status based on lastSeen timestamp */ export function calculateOnlineStatus( lastSeen: Date, thresholds: StatusThresholds = DEFAULT_THRESHOLDS ): OnlineStatus { const minutesAgo = getMinutesAgo(lastSeen); if (minutesAgo < thresholds.onlineMaxMinutes) { return 'online'; } else if (minutesAgo < thresholds.warningMaxMinutes) { return 'warning'; } return 'offline'; } /** * Format last seen time for display */ export function formatLastSeen(lastSeen: Date): string { const minutesAgo = getMinutesAgo(lastSeen); if (minutesAgo < 1) { return 'Just now'; } else if (minutesAgo < 60) { return `${minutesAgo} min ago`; } else if (minutesAgo < 24 * 60) { const hours = Math.floor(minutesAgo / 60); return `${hours} hour${hours > 1 ? 's' : ''} ago`; } else { const days = Math.floor(minutesAgo / (24 * 60)); return `${days} day${days > 1 ? 's' : ''} ago`; } } /** * Get status color for UI display */ export function getStatusColor(status: OnlineStatus): string { switch (status) { case 'online': return '#22c55e'; // green case 'warning': return '#f59e0b'; // amber case 'offline': return '#ef4444'; // red } } /** * Get status label for display */ export function getStatusLabel(status: OnlineStatus): string { switch (status) { case 'online': return 'Online'; case 'warning': return 'Warning'; case 'offline': return 'Offline'; } } /** * Calculate overall status for a group of devices * Returns the "worst" status among all devices */ export function calculateOverallStatus(devices: DeviceStatusInfo[]): OnlineStatus { if (devices.length === 0) { return 'offline'; } // If any device is offline, overall is offline if (devices.some(d => d.status === 'offline')) { return 'offline'; } // If any device has warning, overall is warning if (devices.some(d => d.status === 'warning')) { return 'warning'; } // All devices are online return 'online'; } /** * Transform WPSensor to DeviceStatusInfo */ function sensorToStatusInfo(sensor: WPSensor): DeviceStatusInfo { const lastSeen = sensor.lastSeen instanceof Date ? sensor.lastSeen : new Date(sensor.lastSeen); const minutesAgo = getMinutesAgo(lastSeen); return { deviceId: sensor.deviceId, deviceName: sensor.name, status: sensor.status === 'warning' ? 'warning' : sensor.status as OnlineStatus, lastSeen: lastSeen, lastSeenMinutesAgo: minutesAgo, lastSeenFormatted: formatLastSeen(lastSeen), wellId: sensor.wellId, mac: sensor.mac, }; } /** * Online Status Service * Singleton service for checking device online status */ class OnlineStatusService { private thresholds: StatusThresholds = DEFAULT_THRESHOLDS; private cache = new Map(); private cacheMaxAgeMs = 30000; // 30 seconds cache /** * Set custom status thresholds */ setThresholds(thresholds: Partial): void { this.thresholds = { ...this.thresholds, ...thresholds }; } /** * Get current thresholds */ getThresholds(): StatusThresholds { return { ...this.thresholds }; } /** * Set cache max age */ setCacheMaxAge(maxAgeMs: number): void { this.cacheMaxAgeMs = maxAgeMs; } /** * Clear all cached data */ clearCache(): void { this.cache.clear(); } /** * Check if cached data is still valid */ private isCacheValid(cacheEntry: { timestamp: number }): boolean { return Date.now() - cacheEntry.timestamp < this.cacheMaxAgeMs; } /** * Check online status for a single beneficiary's devices * @param beneficiaryId - The beneficiary ID to check * @param forceRefresh - Skip cache and fetch fresh data * @returns Status check result */ async checkBeneficiaryStatus( beneficiaryId: string, forceRefresh = false ): Promise { // Check cache first (unless force refresh) if (!forceRefresh) { const cached = this.cache.get(beneficiaryId); if (cached && this.isCacheValid(cached)) { return { ok: true, data: cached.data }; } } try { // Fetch devices from API const response = await api.getDevicesForBeneficiary(beneficiaryId); if (!response.ok) { return { ok: false, error: typeof response.error === 'string' ? response.error : response.error?.message || 'Failed to fetch devices', }; } const sensors = response.data || []; // Transform sensors to status info const devices: DeviceStatusInfo[] = sensors.map(sensorToStatusInfo); // Count statuses const onlineCount = devices.filter(d => d.status === 'online').length; const warningCount = devices.filter(d => d.status === 'warning').length; const offlineCount = devices.filter(d => d.status === 'offline').length; const result: BeneficiaryOnlineStatus = { beneficiaryId, totalDevices: devices.length, onlineCount, warningCount, offlineCount, overallStatus: calculateOverallStatus(devices), devices, lastChecked: new Date(), }; // Update cache this.cache.set(beneficiaryId, { data: result, timestamp: Date.now() }); return { ok: true, data: result }; } catch (error) { return { ok: false, error: error instanceof Error ? error.message : 'Unknown error occurred', }; } } /** * Check online status for multiple beneficiaries * @param beneficiaryIds - Array of beneficiary IDs to check * @param forceRefresh - Skip cache and fetch fresh data * @returns Map of beneficiary ID to status result */ async checkMultipleBeneficiaries( beneficiaryIds: string[], forceRefresh = false ): Promise> { const results = new Map(); // Process in parallel but limit concurrency const batchSize = 3; for (let i = 0; i < beneficiaryIds.length; i += batchSize) { const batch = beneficiaryIds.slice(i, i + batchSize); const batchResults = await Promise.all( batch.map(id => this.checkBeneficiaryStatus(id, forceRefresh)) ); batch.forEach((id, index) => { results.set(id, batchResults[index]); }); } return results; } /** * Get cached status for a beneficiary (no API call) * Returns undefined if not cached or cache expired */ getCachedStatus(beneficiaryId: string): BeneficiaryOnlineStatus | undefined { const cached = this.cache.get(beneficiaryId); if (cached && this.isCacheValid(cached)) { return cached.data; } return undefined; } /** * Check if a specific device is online * Convenience method for quick device checks */ async isDeviceOnline(beneficiaryId: string, deviceId: string): Promise { const result = await this.checkBeneficiaryStatus(beneficiaryId); if (!result.ok || !result.data) { return false; } const device = result.data.devices.find(d => d.deviceId === deviceId); return device?.status === 'online'; } /** * Get summary statistics for a beneficiary's devices */ async getStatusSummary( beneficiaryId: string ): Promise<{ online: number; warning: number; offline: number; total: number } | null> { const result = await this.checkBeneficiaryStatus(beneficiaryId); if (!result.ok || !result.data) { return null; } return { online: result.data.onlineCount, warning: result.data.warningCount, offline: result.data.offlineCount, total: result.data.totalDevices, }; } } // Export singleton instance export const onlineStatusService = new OnlineStatusService(); // Export class for testing export { OnlineStatusService };