"use client"; import React, { useEffect, useState, useRef, useCallback } from 'react'; import mqtt from 'mqtt'; import { Menu, Zap } from 'lucide-react'; import Sidebar from '@/components/Sidebar'; import Header from '@/components/Header'; import StatCards from '@/components/StatCards'; import dynamic from 'next/dynamic'; const VoltageChart = dynamic(() => import('@/components/VoltageChart'), { ssr: false }); const HistoryPage = dynamic(() => import('@/components/HistoryPage'), { ssr: false }); import EventLog from '@/components/EventLog'; import EventLogsPage from '@/components/EventLogsPage'; import SettingsPanel from '@/components/SettingsPanel'; import Login from '@/components/Login'; import styles from './page.module.css'; import { insertHistoryPoint, insertEvent, fetchHistory, fetchEvents } from '@/lib/db'; import type { HistoryRow, EventRow } from '@/lib/db'; export interface UpsData { success: number; on: number; state: number; v: string; batt: number; battP: number; line: number; charging: number; chgError: number; uptime?: number; auto?: number; battBlink?: number; battCritical?: number; chargeLv?: number; battMax?: number; timedShutdown?: number; shutdownRemain?: number; shutdownSuggestMode?: number; shutdownSuggest?: number; shutLastTimer?: number; chgMode?: number; chgOVP?: number; chgReducer?: number; chgFullTrig?: number; chgOFC?: number; } export interface UpsEvent { id: string; message: string; time: string; severity?: 'info' | 'success' | 'warning' | 'error'; } // Classify event severity based on message content function classifySeverity(msg: string): 'info' | 'success' | 'warning' | 'error' { const lower = msg.toLowerCase(); if (lower.includes('error') || lower.includes('fail') || lower.includes('disconnect') || lower.includes('offline')) return 'error'; if (lower.includes('warn') || lower.includes('critical') || lower.includes('low')) return 'warning'; if (lower.includes('connect') || lower.includes('online') || lower.includes('subscrib')) return 'success'; return 'info'; } // Convert DB history row to chart point function rowToChartPoint(row: HistoryRow) { return { time: row.created_at ? new Date(row.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }) : '--:--:--', batteryPct: row.battery_pct, vBatt: row.v_batt, }; } // Convert DB event row to UpsEvent function rowToEvent(row: EventRow): UpsEvent { return { id: String(row.id ?? crypto.randomUUID()), message: row.message, time: row.created_at ? new Date(row.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }) : '--:--:--', severity: row.severity, }; } export default function Dashboard() { const [activeTab, setActiveTab] = useState<'dashboard' | 'history' | 'logs' | 'settings'>('dashboard'); const [isSidebarOpen, setIsSidebarOpen] = useState(false); const [upsData, setUpsData] = useState(null); const [mqttStatus, setMqttStatus] = useState<'Connecting...' | 'Online' | 'Offline'>('Connecting...'); const [events, setEvents] = useState([]); const [chartData, setChartData] = useState<{ time: string; batteryPct: number; vBatt: number }[]>([]); const [liveChartData, setLiveChartData] = useState<{ time: string; batteryPct: number; vBatt: number }[]>([]); const [dbLoaded, setDbLoaded] = useState(false); const mqttClientRef = useRef(null); const prevUpsDataRef = useRef(null); const lastMqttMessageRef = useRef(0); // Throttle: only save to Supabase every 30 seconds const lastSaveRef = useRef(0); const addEvent = useCallback((msg: string, persist = true) => { const severity = classifySeverity(msg); const ev: UpsEvent = { id: crypto.randomUUID(), message: msg, time: new Date().toLocaleTimeString(), severity, }; setEvents(prev => [ev, ...prev].slice(0, 200)); // Save to Supabase (for non-trivial messages) if (persist) { insertEvent({ message: msg, severity } as Partial & Pick); } }, []); const clearEvents = async () => { const { clearAllEvents } = await import('@/lib/db'); await clearAllEvents(); setEvents([]); }; // Auto-clear data if no MQTT message received for 10 seconds (device offline) useEffect(() => { const interval = setInterval(() => { if (lastMqttMessageRef.current > 0) { const elapsed = (Date.now() - lastMqttMessageRef.current) / 1000; if (elapsed >= 10 && prevUpsDataRef.current !== null) { setUpsData(null); setLiveChartData([]); prevUpsDataRef.current = null; addEvent('IoT Device went Offline (No telemetry signal for >10s)', true); } } }, 1000); return () => clearInterval(interval); }, [addEvent]); // Initial data load on mount useEffect(() => { const loadInitialData = async () => { try { const [histRows, evRows] = await Promise.all([ fetchHistory(100), fetchEvents(200) ]); if (histRows.length > 0) { const points = histRows.map(rowToChartPoint); setChartData(points); } if (evRows.length > 0) { const evs = evRows .map(rowToEvent) .filter(ev => !ev.message.startsWith('Connected to') && !ev.message.startsWith('Subscribed to') && !ev.message.startsWith('Connecting to') && !ev.message.startsWith('Subscription error') && !ev.message.startsWith('MQTT Connection Offline') ); setEvents(evs); } setDbLoaded(true); } catch (err) { console.warn('[DB] initial load warning:', err); } }; loadInitialData(); }, []); // MQTT Connection setup useEffect(() => { setMqttStatus('Connecting...'); console.log('Connecting to MQTT Broker (wss://broker.emqx.io:8084/mqtt)...'); const client = mqtt.connect('wss://broker.emqx.io:8084/mqtt'); mqttClientRef.current = client; client.on('connect', () => { setMqttStatus('Online'); console.log('Connected to MQTT Broker'); client.subscribe('ups/dev15/state', (err) => { if (err && err.message !== 'client disconnecting') { console.warn(`Subscription warning: ${err.message}`); } }); }); client.on('message', (topic, message) => { if (topic === 'ups/dev15/state') { try { // Sanitize the string: remove control characters like \n, \t, etc. that might break JSON parse const rawString = message.toString().replace(/[\x00-\x1F\x7F-\x9F]/g, ""); const data: UpsData = JSON.parse(rawString); lastMqttMessageRef.current = Date.now(); setUpsData(data); // Track state changes to generate event logs const prevData = prevUpsDataRef.current; if (prevData) { // Power Source change if (prevData.line !== data.line) { if (data.line > 0) addEvent('AC Power Restored (Line Active)', true); else addEvent('AC Power Lost (Running on Battery)', true); } // Output State change if (prevData.on !== data.on) { if (data.on === 1) addEvent('UPS Output turned ON', true); else addEvent('UPS Output turned OFF', true); } // Battery Critical if (prevData.battCritical !== data.battCritical && data.battCritical === 1) { addEvent('WARNING: Battery level is CRITICAL', true); } // Charging state if (prevData.charging !== data.charging) { if (data.charging === 0 && data.battP >= 95) addEvent('Battery Fully Charged', true); else if (data.charging === 1) addEvent('Battery is now charging', false); } // Shutdown Timer if ((prevData.shutdownRemain ?? 0) === 0 && (data.shutdownRemain ?? 0) > 0) { addEvent(`Shutdown timer started (${data.shutdownRemain}s)`, true); } else if ((prevData.shutdownRemain ?? 0) > 0 && (data.shutdownRemain ?? 0) === 0 && data.on === 1) { addEvent('Shutdown timer cancelled', true); } } prevUpsDataRef.current = data; const newPoint = { time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }), batteryPct: data.battP, vBatt: parseFloat(data.v), }; setChartData(prev => [...prev, newPoint].slice(-100)); setLiveChartData(prev => [...prev, newPoint].slice(-15)); // Throttled save to Supabase (every 30 seconds) const now = Date.now(); if (now - lastSaveRef.current >= 30000) { lastSaveRef.current = now; insertHistoryPoint({ v_batt: parseFloat(data.v), battery_pct: data.battP, line_state: data.line, power_on: data.on === 1, }); } } catch (e) { // Changed to console.warn to prevent Next.js from throwing a red error overlay in dev mode console.warn('JSON parse error (likely malformed MQTT data):', e); } } }); client.on('offline', () => { setMqttStatus('Offline'); console.log('MQTT Connection Offline'); }); return () => { client.end(); }; }, [dbLoaded, addEvent]); return (
setIsSidebarOpen(false)} />
UPS Monitor
{activeTab === 'dashboard' &&
} {activeTab === 'dashboard' && ( <>
!ev.message.startsWith('Connected to') && !ev.message.startsWith('Subscribed to') && !ev.message.startsWith('Connecting to')) : []} />
)} {activeTab === 'history' && ( )} {activeTab === 'logs' && } {activeTab === 'settings' && { if (mqttClientRef.current && mqttClientRef.current.connected) { const payload = JSON.stringify({ cmd, val: String(val) }); mqttClientRef.current.publish('ups/dev15/cmd', payload); addEvent(`Sent command: ${cmd}=${val}`, false); } else { alert("MQTT is not connected. Cannot send command."); } }} />}
); }