diff --git a/SampleJSON.txt b/SampleJSON.txt index d459e35..fc2e3c8 100644 --- a/SampleJSON.txt +++ b/SampleJSON.txt @@ -1,6 +1,6 @@ { - "Timestamp": "2025-01-16T10:55:58.9084242Z", - "SystemId": "Maureen Jonas", + "Timestamp": "2025-01-17T09:54:24.8502369Z", + "SystemId": "Michael DeBakey", "StatusData": { "ExtLeft": { "Text": "OK", @@ -15,15 +15,15 @@ "Color": "badge-success" }, "BytesSent": { - "Text": "52", + "Text": "8", "Color": "badge-info" }, "BytesRecd": { - "Text": "0.08", + "Text": "4.87", "Color": "badge-info" }, "Strokes": { - "Text": "2,338", + "Text": "155,794", "Color": "badge-info" }, "IntLeft": { @@ -35,7 +35,7 @@ "Color": "#FF2EAE00" }, "BusLoad": { - "Text": "5%", + "Text": "0%", "Color": "#FF2EAE00" } }, @@ -47,7 +47,7 @@ "BackColor": "Default" }, "IntPressure": { - "PrimaryValue": "2.5", + "PrimaryValue": "3", "SecondaryValue": null, "BackColor": "Yellow" }, @@ -56,28 +56,28 @@ "SecondaryValue": null, "BackColor": "Default" }, - "IntPressureMin": 2, - "IntPressureMax": 3, + "IntPressureMin": 2.79999995, + "IntPressureMax": 4.19999981, "CardiacOutput": { - "PrimaryValue": "5", + "PrimaryValue": "4.9", "SecondaryValue": null, - "BackColor": "Default" + "BackColor": "Yellow" }, - "ActualStrokeLen": "20.4", + "ActualStrokeLen": "19", "TargetStrokeLen": "21.8", - "SensorTemperature": "21.99", - "ThermistorTemperature": "21.99", + "SensorTemperature": "24.9", + "ThermistorTemperature": "24.9", "CpuLoad": "73" }, "RightHeart": { - "StrokeVolume": "40", + "StrokeVolume": "39", "PowerConsumption": { - "PrimaryValue": "0.2", + "PrimaryValue": "0.1", "SecondaryValue": null, "BackColor": "Default" }, "IntPressure": { - "PrimaryValue": "9.5", + "PrimaryValue": "10", "SecondaryValue": null, "BackColor": "Default" }, @@ -86,26 +86,26 @@ "SecondaryValue": null, "BackColor": "Default" }, - "IntPressureMin": 7.5999999, - "IntPressureMax": 11.3999996, + "IntPressureMin": 8.80000019, + "IntPressureMax": 13.1990004, "CardiacOutput": { "PrimaryValue": "5.3", "SecondaryValue": null, "BackColor": "Default" }, - "ActualStrokeLen": "16.4", - "TargetStrokeLen": "18.1", - "SensorTemperature": "21.99", - "ThermistorTemperature": "28.57", + "ActualStrokeLen": "15.8", + "TargetStrokeLen": "17.7", + "SensorTemperature": "24.9", + "ThermistorTemperature": "24.02", "CpuLoad": "73" }, - "HeartRate": "144", - "OperationState": "Auto", + "HeartRate": "148", + "OperationState": "Manual", "HeartStatus": "Both Running", - "FlowLimitState": "", - "FlowLimit": "9.193", + "FlowLimitState": "Decrease", + "FlowLimit": "9.199", "AtmosPressure": "0", - "UseMedicalSensor": true, + "UseMedicalSensor": false, "AoPSensor": { "PrimaryValue": "-", "SecondaryValue": null, @@ -127,6 +127,6 @@ "BackColor": "Default" }, "IVCSensorVal": "-", - "LocalClock": "11:55:58", + "LocalClock": "10:54:24", "Messages": [] } \ No newline at end of file diff --git a/dist/index.html b/dist/index.html index 94defee..0bc048e 100644 --- a/dist/index.html +++ b/dist/index.html @@ -5,8 +5,8 @@ SRH Remote Monitor 1.0.0 - - + +
diff --git a/package.json b/package.json index ed90fb5..cc8da0f 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "start": "node server/server.js", "dev": "concurrently \"node server/server.js\" \"vite\"", "build": "vite build", - "deploy": "npm run build && scp -r dist package.json index.html root@realheartremote.live:/var/www/realheartremote.live/ && ssh root@realheartremote.live \"cd /var/www/realheartremote.live && npm install && pm2 restart all\"", + "deploy": "npm run build && scp -r dist package.json index.html server root@realheartremote.live:/var/www/realheartremote.live/ && ssh root@realheartremote.live \"cd /var/www/realheartremote.live && npm install && pm2 restart all\"", "preview": "vite preview", "build:css": "tailwindcss -i ./src/input.css -o ./public/styles.css --minify", "watch:css": "tailwindcss -i ./src/input.css -o ./public/styles.css --watch" diff --git a/public/cpu.png b/public/cpu.png new file mode 100644 index 0000000..f01b9ce Binary files /dev/null and b/public/cpu.png differ diff --git a/public/temperature.png b/public/temperature.png new file mode 100644 index 0000000..5457028 Binary files /dev/null and b/public/temperature.png differ diff --git a/server/server.js b/server/server.js index 0001d73..29682d1 100644 --- a/server/server.js +++ b/server/server.js @@ -1,6 +1,9 @@ const express = require('express'); -const { WebSocketServer } = require('ws'); +const { WebSocketServer, WebSocket } = require('ws'); const http = require('http'); +const fs = require('fs').promises; +const fsSync = require('fs'); +const path = require('path'); const app = express(); const server = http.createServer(app); @@ -217,36 +220,44 @@ wss.on('connection', (ws, req) => // Broadcast connection message const connectionMessage = { - type: 'deviceMessage', + type: 'chatMessage', timestamp: new Date().toISOString(), username: displayName, message: `${displayName} just connected` }; wss.clients.forEach((client) => { - if (client.readyState === ws.OPEN) { + if (client.readyState === WebSocket.OPEN) { client.send(JSON.stringify(connectionMessage)); } }); - ws.on('message', (data) => { + ws.on('message', async (data) => { try { const rawData = data.toString(); const message = JSON.parse(rawData); + //console.log('Received message:', message); + + // Log the message + await appendToLog(message).catch(err => console.error('Failed to log message:', err)); + // Handle array of system messages if (Array.isArray(message) && message.length > 0 && message[0].Type === 'systemMessage') { + //console.log('Received system messages:', message); if (ws.systemId) { const watchingClients = connectedSystems.get(ws.systemId); if (watchingClients) { message.forEach(msg => { + console.log('Processing message:', msg); const systemMessage = { type: 'systemMessage', message: msg.Message, messageType: msg.MessageType, - source: msg.Source, + source: msg.Source || wsClients.get(ws) || 'Unknown User', timestamp: msg.Timestamp }; + //console.log('Sending formatted message:', systemMessage); watchingClients.forEach(client => { if (client !== ws && client.readyState === WebSocket.OPEN) { @@ -255,79 +266,80 @@ wss.on('connection', (ws, req) => }); }); } + return; } - return; - } - // Track system if SystemId is present in message - if (message.SystemId) { - const systemId = message.SystemId; + // Track system if SystemId is present in message + if (message.SystemId) { + const systemId = message.SystemId; - // Clean up any duplicates before adding new system - cleanupDuplicateSystems(systemId); + // Clean up any duplicates before adding new system + cleanupDuplicateSystems(systemId); - // Add or update system connection if not already tracked - if (!connectedSystems.has(systemId)) { - connectedSystems.set(systemId, new Set()); - } - - // Mark this connection as the system if not already marked - if (!ws.isSystem) { - ws.isSystem = true; - ws.systemId = systemId; - // Broadcast updated systems list after marking as system - broadcastSystemsList(); - } + // Add or update system connection if not already tracked + if (!connectedSystems.has(systemId)) { + connectedSystems.set(systemId, new Set()); + } + + // Mark this connection as the system if not already marked + if (!ws.isSystem) { + ws.isSystem = true; + ws.systemId = systemId; + // Broadcast updated systems list after marking as system + broadcastSystemsList(); + } - // Forward status update to watching clients - const watchingClients = connectedSystems.get(systemId); - if (watchingClients) { - // Forward the status update - watchingClients.forEach(client => { - if (client !== ws && client.readyState === WebSocket.OPEN) { - client.send(rawData); - } - }); + // Forward status update to watching clients + const watchingClients = connectedSystems.get(systemId); + if (watchingClients) { + // Forward the status update + watchingClients.forEach(client => { + if (client !== ws && client.readyState === WebSocket.OPEN) { + client.send(rawData); + } + }); - // Process any messages in the status update - if (message.Messages && Array.isArray(message.Messages)) { - message.Messages.forEach(msg => { - const systemMessage = { - type: 'systemMessage', - message: msg.Message, - messageType: msg.MessageType, - source: msg.Source, - timestamp: msg.Timestamp - }; - - watchingClients.forEach(client => { - if (client !== ws && client.readyState === WebSocket.OPEN) { - client.send(JSON.stringify(systemMessage)); - } + // Process any messages in the status update + if (message.Messages && Array.isArray(message.Messages)) { + message.Messages.forEach(msg => { + const systemMessage = { + type: 'systemMessage', + message: msg.Message, + messageType: msg.MessageType, + source: msg.Source, + timestamp: msg.Timestamp, + username: wsClients.get(ws) || 'Unknown User' + }; + + watchingClients.forEach(client => { + if (client !== ws && client.readyState === WebSocket.OPEN) { + client.send(JSON.stringify(systemMessage)); + } + }); }); - }); + } } } - } - // Handle device messages - else if (message?.type === 'deviceMessage') { - // Format the display name from the email - const displayName = formatDisplayName(message.email); - - // Send to all clients, marking the message as "self" for the sender - wss.clients.forEach((client) => { - if (client.readyState === WebSocket.OPEN) { - const broadcastMessage = { - type: 'deviceMessage', - timestamp: new Date().toISOString(), - username: displayName, - message: message.message, - self: client === ws - }; - client.send(JSON.stringify(broadcastMessage)); - } - }); + // Handle device messages + else if (message?.type === 'deviceMessage' || message?.type === 'chatMessage') { + // Get the display name from message or stored client info + const displayName = message.username || wsClients.get(ws) || 'Unknown User'; + + // Send to all clients, marking the message as "self" for the sender + wss.clients.forEach((client) => { + if (client.readyState === WebSocket.OPEN) { + const broadcastMessage = { + type: 'chatMessage', + timestamp: message.timestamp || new Date().toISOString(), + username: displayName, + message: message.message, + self: client === ws + }; + client.send(JSON.stringify(broadcastMessage)); + } + }); + } } } catch (error) { console.error('Error processing message:', error); @@ -335,7 +347,7 @@ wss.on('connection', (ws, req) => try { const email = wsClients.get(ws) || 'Unknown User'; const broadcastMessage = { - type: 'deviceMessage', + type: 'chatMessage', timestamp: new Date().toISOString(), username: formatDisplayName(email), message: data.toString() @@ -369,7 +381,7 @@ wss.on('connection', (ws, req) => if (connectedSystems.size === 0) { const clients = connectedSystems.get(ws.systemId) || new Set(); clients.forEach(client => { - if (client.readyState === ws.OPEN) { + if (client.readyState === WebSocket.OPEN) { waitingClients.add(client); client.systemToWatch = null; } @@ -383,6 +395,76 @@ wss.on('connection', (ws, req) => }); }); +const LOG_FILE = path.join(__dirname, 'logdata.json'); +/** @type {Array<{timestamp: string, type: string, data: any}>} */ +let messageLog = []; +/** @type {fs.WriteStream} */ +let logStream; + +/** + * Initialize the log file and create write stream + * @returns {Promise} + */ +async function initializeLogFile() { + try { + await fs.access(LOG_FILE); + const data = await fs.readFile(LOG_FILE, 'utf8'); + messageLog = JSON.parse(data); + } catch { + await fs.writeFile(LOG_FILE, JSON.stringify([], null, 2)); + } + + // Create write stream in append mode + logStream = fsSync.createWriteStream(LOG_FILE, { + flags: 'a', + encoding: 'utf8' + }); + + // Handle stream errors + logStream.on('error', (error) => { + console.error('Error writing to log file:', error); + }); + + console.log('Log file initialized'); +} + +/** + * Append message to log file using write stream + * @param {any} message - The message to log + * @returns {Promise} + */ +async function appendToLog(message) { + const logEntry = { + timestamp: new Date().toISOString(), + type: Array.isArray(message) ? 'systemMessages' : 'message', + data: message + }; + messageLog.push(logEntry); + + // Write to stream with newline for easier reading + return new Promise((resolve, reject) => { + logStream.write(JSON.stringify(logEntry) + '\n', (error) => { + if (error) reject(error); + else resolve(); + }); + }); +} + +// Clean up write stream when server exits +process.on('SIGINT', () => { + if (logStream) { + logStream.end(() => { + console.log('Log file stream closed'); + process.exit(0); + }); + } +}); + +// Initialize log file when server starts +console.log('Opening Log file...'); + +initializeLogFile().catch(console.error); + server.listen(3000, () => { console.log('Server is running on port 3000'); }); diff --git a/server/server2.js b/server/server2.js new file mode 100644 index 0000000..fea74f6 --- /dev/null +++ b/server/server2.js @@ -0,0 +1,323 @@ +const express = require('express'); +const { WebSocketServer } = require('ws'); +const http = require('http'); +const fs = require('fs').promises; +const fsSync = require('fs'); +const path = require('path'); + +const app = express(); +const server = http.createServer(app); +const wss = new WebSocketServer({ server }); + +app.use(express.json()); + +// Store WebSocket to email mapping +const wsClients = new Map(); + +// Initialize logging +const LOG_FILE = path.join(__dirname, 'logdata.json'); +/** @type {Array<{timestamp: string, type: string, data: any}>} */ +let messageLog = []; +/** @type {fs.WriteStream} */ +let logStream; + +/** + * Initialize the log file and create write stream + * @returns {Promise} + */ +async function initializeLogFile() { + try { + await fs.access(LOG_FILE); + const data = await fs.readFile(LOG_FILE, 'utf8'); + messageLog = JSON.parse(data); + } catch { + await fs.writeFile(LOG_FILE, JSON.stringify([], null, 2)); + } + + // Create write stream in append mode + logStream = fsSync.createWriteStream(LOG_FILE, { + flags: 'a', + encoding: 'utf8' + }); + + // Handle stream errors + logStream.on('error', (error) => { + console.error('Error writing to log file:', error); + }); + + console.log('Log file initialized'); +} + +/** + * Append message to log file using write stream + * @param {any} message - The message to log + * @returns {Promise} + */ +async function appendToLog(message) { + const logEntry = { + timestamp: new Date().toISOString(), + type: Array.isArray(message) ? 'systemMessages' : 'message', + data: message + }; + messageLog.push(logEntry); + + // Write to stream with newline for easier reading + return new Promise((resolve, reject) => { + logStream.write(JSON.stringify(logEntry) + '\n', (error) => { + if (error) reject(error); + else resolve(); + }); + }); +} + +// Clean up write stream when server exits +process.on('SIGINT', () => { + if (logStream) { + logStream.end(() => { + console.log('Log file stream closed'); + process.exit(0); + }); + } +}); + +// Format display name from email +const formatDisplayName = (email) => { + if (!email) return 'Someone'; + + if (email.endsWith('@realheart.se')) { + const [name] = email.split('@'); + const [firstName, lastName] = name.split('.'); + if (firstName && lastName) { + return `${firstName.charAt(0).toUpperCase() + firstName.slice(1)} ${lastName.charAt(0).toUpperCase() + lastName.slice(1)}`; + } + } + return email; +}; + +// Handle uncaught exceptions +process.on('uncaughtException', (err) => +{ + console.error('Uncaught Exception:', err); + process.exit(1); // This will trigger PM2 to restart the process + }); + +// Handle unhandled promise rejections +process.on('unhandledRejection', (reason, promise) => +{ + console.error('Unhandled Rejection at:', promise, 'reason:', reason); + process.exit(1); +}); + +// Optional: Handle SIGTERM signal +process.on('SIGTERM', () => +{ + console.info('SIGTERM signal received'); + + // Close WebSocket server gracefully + wss.close(() => { + console.log('WebSocket server closed'); + + // Close HTTP server gracefully + server.close(() => { + console.log('HTTP server closed'); + process.exit(0); + }); + }); + + // Force exit if graceful shutdown fails + setTimeout(() => + { + console.error('Forced shutdown'); + process.exit(1); + }, 10000); // Force shutdown after 10 seconds +}); + +function isCriticalError(error) +{ + const criticalErrors = [ + 'ECONNRESET', + 'EPIPE', + 'ERR_STREAM_DESTROYED' + ]; + + return criticalErrors.includes(error.code); + } + +// WebSocket connection handling +wss.on('connection', (ws, req) => +{ + ws.on('error', (error) => { + console.error('WebSocket error:', error); + if (isCriticalError(error)) + { + process.exit(1); + } + }); + + // Get email from URL parameters + const url = new URL(req.url, `http://${req.headers.host}`); + const connectionType = url.searchParams.get('type'); + const displayName = connectionType === 'device' + ? url.searchParams.get('device-name') || 'Unknown Device' + : formatDisplayName(url.searchParams.get('email')); + + // Broadcast connection message + const connectionMessage = { + type: 'deviceMessage', + timestamp: new Date().toISOString(), + username: displayName, + message: `${displayName} just connected` + }; + + wss.clients.forEach((client) => { + if (client.readyState === ws.OPEN) { + client.send(JSON.stringify(connectionMessage)); + } + }); + + ws.on('message', async (data) => { + try { + const message = JSON.parse(data.toString()); + let dataString = data.toString(); + + // Log the message + await appendToLog(message).catch(err => console.error('Failed to log message:', err)); + + // Handle device messages differently from data updates + if (message?.type === 'deviceMessage') + { + // Format the display name from the email + const displayName = formatDisplayName(message.email); + + // Send to all clients, marking the message as "self" for the sender + wss.clients.forEach((client) => { + if (client.readyState === ws.OPEN) { + const broadcastMessage = { + type: 'deviceMessage', + timestamp: new Date().toISOString(), + username: displayName, + message: message.message, + self: client === ws + }; + client.send(JSON.stringify(broadcastMessage)); + } + }); + } + else if (message?.type === 'deviceData') + { + // Forward the original data to other clients + wss.clients.forEach((client) => { + if (client !== ws && client.readyState === ws.OPEN) { + // Send the original data first + client.send(dataString); + + // If there are messages in the data, send them separately + if (message.Messages && Array.isArray(message.Messages)) { + message.Messages.forEach(msg => { + const systemMessage = { + type: 'systemMessage', + message: msg.Message, + messageType: msg.MessageType, + source: msg.Source, + timestamp: msg.Timestamp + }; + client.send(JSON.stringify(systemMessage)); + }); + } + } + }); + } + else + { + // Handle other data updates (broadcast to other clients only) + wss.clients.forEach((client) => { + if (client !== ws && client.readyState === ws.OPEN) { + // Ensure UseMedicalSensor is boolean before sending + let dataToSend = dataString; + if (message.UseMedicalSensor !== undefined) { + const modifiedMessage = { ...message }; + modifiedMessage.UseMedicalSensor = modifiedMessage.UseMedicalSensor === true || modifiedMessage.UseMedicalSensor === 'true'; + dataToSend = JSON.stringify(modifiedMessage); + } + // Send the data + client.send(dataToSend); + + // If there are messages in the data, send them separately + if (message.Messages && Array.isArray(message.Messages)) { + message.Messages.forEach(msg => { + const systemMessage = { + type: 'systemMessage', + message: msg.Message, + messageType: msg.MessageType, + source: msg.Source, + timestamp: msg.Timestamp + }; + client.send(JSON.stringify(systemMessage)); + }); + } + } + }); + } + } + catch (error) + { + console.error('Error processing message:', error); + // Don't forward raw messages on parse error + // Instead, try to handle it as a device message if possible + try { + const email = wsClients.get(ws) || 'Unknown User'; + const broadcastMessage = { + type: 'deviceMessage', + timestamp: new Date().toISOString(), + username: formatDisplayName(email), + message: data.toString() + }; + + const broadcastString = JSON.stringify(broadcastMessage); + + // Send to ALL clients INCLUDING the sender + wss.clients.forEach((client) => { + if (client.readyState === ws.OPEN) { + client.send(broadcastString); + } + }); + } + catch (innerError) + { + console.error('Error broadcasting message:', innerError); + } + } + }); + + ws.on('close', () => { + console.log('Client disconnected'); + wsClients.delete(ws); + }); +}); + +server.listen(3000, () => { + console.log('Server is running on port 3000'); +}); + +function checkCriticalConditions() { + // Example: Check memory usage + const usedMemory = process.memoryUsage().heapUsed / 1024 / 1024; + if (usedMemory > 400) { // More than 400MB since server only has 512MB RAM + console.error('Memory threshold exceeded'); + process.exit(1); + } + + // Example: Check WebSocket connections + const clientCount = wss.clients.size; + if (clientCount > 100) { // Too many connections + console.error('Too many WebSocket connections'); + process.exit(1); + } + } + + // Run checks periodically + setInterval(checkCriticalConditions, 60000); + +// Initialize log file when server starts +console.log('Opening Log file...'); +initializeLogFile().catch(console.error); \ No newline at end of file diff --git a/src/components/Components.jsx b/src/components/Components.jsx index 2f046c8..81bd2f4 100644 --- a/src/components/Components.jsx +++ b/src/components/Components.jsx @@ -32,6 +32,14 @@ export function createDetailCard(label, value, iconFile = 'heart.png', color = ' stateValue = "-"; stateDesc = 'Flow State'; } + if(units === '°C') + { + detailedData.LeftHeart.formattedTemp = Number(detailedData.LeftHeart.formattedTemp).toFixed(1); + } + if(units === '%') + { + detailedData.LeftHeart.CpuLoad = Number(detailedData.LeftHeart.CpuLoad ).toFixed(1); + } return React.createElement('div', { className: 'stat bg-base-300 shadow-xl rounded-xl p-4' }, React.createElement('div', { className: 'flex justify-between items-start' }, @@ -209,7 +217,7 @@ export function createStrokeCard(label, targetStroke, actualStroke, iconFile = ' return React.createElement('div', { className: `stat ${cardBgColor} shadow-xl rounded-xl p-4` }, React.createElement('div', { className: 'flex justify-between items-start' }, React.createElement('div', { className: 'flex-1 min-w-0 pr-4' }, - React.createElement('div', { className: 'stat-title opacity-70' }, label), + React.createElement('div', { className: 'stat-title opacity-70 pt-1' }, label), React.createElement('div', { className: 'stat-value text-base-content text-2xl' }, displayValue ), @@ -352,8 +360,8 @@ export function createHeader(status, lastUpdate, isDetailedView, onToggleView, t return timestamp; }; - return React.createElement('div', { className: 'flex flex-wrap justify-between items-center mb-4 gap-2' }, - React.createElement('div', null, + return React.createElement('div', { className: 'flex flex-col sm:flex-row justify-between items-center mb-4 gap-2 w-full' }, + React.createElement('div', { className: 'w-full sm:w-auto order-1' }, React.createElement('h2', { className: 'text-lg font-bold items-center gap-2 w-44' }, 'Status', React.createElement('div', { @@ -369,7 +377,14 @@ export function createHeader(status, lastUpdate, isDetailedView, onToggleView, t `Remote: ${systemId}` ) ), - React.createElement('div', { className: 'flex gap-2' }, + + React.createElement('img', { + src: theme === 'light' ? '/logo-light-mode.png' : '/logo.png', + alt: 'Scandinavian Real Heart AB', + className: 'h-8 order-2 sm:order-3 my-2 sm:my-0' + }), + + React.createElement('div', { className: 'flex gap-2 order-3 sm:order-2 w-full sm:w-auto justify-center' }, React.createElement('button', { className: 'btn btn-primary w-[120px] sm:w-[200px] px-2 sm:px-4 text-sm sm:text-base shadow-lg', onClick: () => { @@ -383,11 +398,6 @@ export function createHeader(status, lastUpdate, isDetailedView, onToggleView, t onClick: onOpenChat }, 'Send Message') ), - React.createElement('img', { - src: theme === 'light' ? '/logo-light-mode.png' : '/logo.png', - alt: 'Scandinavian Real Heart AB', - className: 'h-8 ml-4 mr-4' - }) ); } diff --git a/src/components/MessageLog.jsx b/src/components/MessageLog.jsx index 1fd5656..02b003b 100644 --- a/src/components/MessageLog.jsx +++ b/src/components/MessageLog.jsx @@ -28,9 +28,33 @@ const MessageLog = ({ messages = [] }) => { } }; + const formatTime = (timestamp) => { + const date = new Date(timestamp); + const mm = String(date.getMonth() + 1).padStart(2, '0'); + const dd = String(date.getDate()).padStart(2, '0'); + const yy = String(date.getFullYear()).slice(-2); + const hh = String(date.getHours()).padStart(2, '0'); + const min = String(date.getMinutes()).padStart(2, '0'); + const ss = String(date.getSeconds()).padStart(2, '0'); + return `${mm}/${dd}/${yy} ${hh}:${min}:${ss}`; + }; + + const formatMessage = (msg) => { + // All messages now use source field consistently + return msg.source ? `${msg.source}: ${msg.message}` : msg.message; + }; + // Create a copy and reverse to show newest first const sortedMessages = [...messages].sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)); + // Debug log to check message contents + console.log('Messages:', sortedMessages.map(msg => ({ + source: msg?.source, + message: msg?.message, + messageType: msg?.messageType, + timestamp: msg?.timestamp + }))); + return (
@@ -49,7 +73,7 @@ const MessageLog = ({ messages = [] }) => {
- {msg?.timestamp || '-'} + {formatTime(msg?.timestamp)} {msg?.source || '-'} {msg?.message || '-'} diff --git a/src/pages/dashboard/Dashboard.jsx b/src/pages/dashboard/Dashboard.jsx index 6f3806d..e5f4597 100644 --- a/src/pages/dashboard/Dashboard.jsx +++ b/src/pages/dashboard/Dashboard.jsx @@ -52,6 +52,9 @@ function CombinedDashboard() { CardiacOutput: '0', TargetStrokeLen: '0', ActualStrokeLen: '0', + SensorTemperature: '0', + ThermistorTemperature: '0', + CpuLoad: '0', MedicalPressure: { Name: '', PrimaryValue: '-', @@ -70,6 +73,9 @@ function CombinedDashboard() { CardiacOutput: '0', TargetStrokeLen: '0', ActualStrokeLen: '0', + SensorTemperature: '0', + ThermistorTemperature: '0', + CpuLoad: '0', MedicalPressure: { Name: '', PrimaryValue: '-', @@ -200,17 +206,23 @@ function CombinedDashboard() { const handleWebSocketMessage = React.useCallback((event) => { try { const data = JSON.parse(event.data); - if (data.type === 'deviceMessage') { - addMessage(data); + if (data.type === 'deviceMessage' || data.type === 'chatMessage') { + addMessage({ + type: 'chatMessage', + source: data.username || data.source || 'Unknown User', + message: data.message, + timestamp: data.timestamp || new Date().toISOString() + }); return; } if (data.type === 'systemMessage') { addMessage({ - username: data.source || 'System', + type: 'systemMessage', + source: data.source || 'System', message: data.message, timestamp: data.timestamp, - type: data.messageType + messageType: data.messageType }); return; } @@ -238,10 +250,11 @@ function CombinedDashboard() { if (data.Messages && Array.isArray(data.Messages)) { data.Messages.forEach(msg => { addMessage({ - username: msg.Source || 'System', + type: 'systemMessage', + source: msg.Source || 'System', message: msg.Message, timestamp: msg.Timestamp, - type: msg.MessageType + messageType: msg.MessageType }); }); } @@ -270,7 +283,10 @@ function CombinedDashboard() { CardiacOutput: data.LeftHeart?.CardiacOutput || prevData.LeftHeart.CardiacOutput, MedicalPressure: data.LeftHeart?.MedicalPressure || prevData.LeftHeart.MedicalPressure, TargetStrokeLen: data.LeftHeart?.TargetStrokeLen || prevData.LeftHeart.TargetStrokeLen, - ActualStrokeLen: data.LeftHeart?.ActualStrokeLen || prevData.LeftHeart.ActualStrokeLen + ActualStrokeLen: data.LeftHeart?.ActualStrokeLen || prevData.LeftHeart.ActualStrokeLen, + SensorTemperature: Math.round((data.LeftHeart?.SensorTemperature || prevData.LeftHeart.SensorTemperature + Number.EPSILON) * 10) / 10, + ThermistorTemperature: Math.round((data.LeftHeart?.ThermistorTemperature || prevData.LeftHeart.ThermistorTemperature + Number.EPSILON) * 10) / 10, + CpuLoad: Math.round((data.LeftHeart?.CpuLoad || prevData.LeftHeart.CpuLoad + Number.EPSILON) * 10) / 10 }, RightHeart: { StrokeVolume: data.RightHeart?.StrokeVolume || prevData.RightHeart.StrokeVolume, @@ -282,7 +298,10 @@ function CombinedDashboard() { CardiacOutput: data.RightHeart?.CardiacOutput || prevData.RightHeart.CardiacOutput, MedicalPressure: data.RightHeart?.MedicalPressure || prevData.RightHeart.MedicalPressure, TargetStrokeLen: data.RightHeart?.TargetStrokeLen || prevData.RightHeart.TargetStrokeLen, - ActualStrokeLen: data.RightHeart?.ActualStrokeLen || prevData.RightHeart.ActualStrokeLen + ActualStrokeLen: data.RightHeart?.ActualStrokeLen || prevData.RightHeart.ActualStrokeLen, + SensorTemperature: Math.round((data.RightHeart?.SensorTemperature || prevData.RightHeart.SensorTemperature + Number.EPSILON) * 10) / 10, + ThermistorTemperature: Math.round((data.RightHeart?.ThermistorTemperature || prevData.RightHeart.ThermistorTemperature + Number.EPSILON) * 10) / 10, + CpuLoad: Math.round((data.RightHeart?.CpuLoad || prevData.RightHeart.CpuLoad + Number.EPSILON) * 10) / 10 }, HeartRate: data.HeartRate || prevData.HeartRate, CVPSensor: data.CVPSensor || prevData.CVPSensor, @@ -356,10 +375,13 @@ function CombinedDashboard() { const handleSendMessage = React.useCallback((message) => { if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { const email = user.emailAddresses[0].emailAddress; + const username = user.fullName || email.split('@')[0]; const messageData = { - type: 'deviceMessage', + type: 'chatMessage', message: message, - email: user.emailAddresses[0].emailAddress + email: email, + username: username, + timestamp: new Date().toISOString() }; wsRef.current.send(JSON.stringify(messageData)); @@ -428,17 +450,8 @@ function CombinedDashboard() { React.createElement('div', { className: 'card-body px-1.5 py-0.5' }, React.createElement('h2', { className: 'card-title opacity-80 mt-0 py-0' }, 'System Status'), - React.createElement('div', { className: 'grid grid-cols-2 sm:grid-cols-4 gap-4' }, - createCard('Heart Rate', `${detailedData.HeartRate} BPM`, 'red'), - createCard('Operation State', detailedData.OperationState, 'blue'), - createCard('Heart Status', detailedData.HeartStatus, 'green'), - React.createElement('div', { className: 'grid grid-rows-2 gap-2 py-1' }, - createSensorStatusCard('Medical Sensors', detailedData.UseMedicalSensor), - createSensorStatusCard('Internal Sensors', !detailedData.UseMedicalSensor) - ) - ), - React.createElement('div', { className: 'flex justify-between items-start mt-2 mb-2 flex-wrap gap-2' }, - React.createElement('div', { className: 'flex-1 flex flex-col items-center' }, + React.createElement('div', { className: 'grid grid-cols-5 sm:flex sm:justify-between sm:items-start mt-2 mb-2 gap-x-1 gap-y-2 sm:flex-wrap sm:gap-2' }, + React.createElement('div', { className: 'flex flex-col items-center sm:flex-1' }, React.createElement('span', { className: 'badge w-full text-center', style: { @@ -448,7 +461,7 @@ function CombinedDashboard() { }, detailedData.StatusData.ExtLeft.Text), React.createElement('span', { className: 'text-xs mt-1' }, 'Ext L') ), - React.createElement('div', { className: 'flex-1 flex flex-col items-center' }, + React.createElement('div', { className: 'flex flex-col items-center sm:flex-1' }, React.createElement('span', { className: 'badge w-full text-center', style: { @@ -458,7 +471,7 @@ function CombinedDashboard() { }, detailedData.StatusData.ExtRight.Text), React.createElement('span', { className: 'text-xs mt-1' }, 'Ext R') ), - React.createElement('div', { className: 'flex-1 flex flex-col items-center' }, + React.createElement('div', { className: 'flex flex-col items-center sm:flex-1' }, React.createElement('span', { className: 'badge w-full text-center', style: { @@ -468,7 +481,7 @@ function CombinedDashboard() { }, detailedData.StatusData.IntLeft.Text), React.createElement('span', { className: 'text-xs mt-1' }, 'Int Lt') ), - React.createElement('div', { className: 'flex-1 flex flex-col items-center' }, + React.createElement('div', { className: 'flex flex-col items-center sm:flex-1' }, React.createElement('span', { className: 'badge w-full text-center', style: { @@ -478,23 +491,23 @@ function CombinedDashboard() { }, detailedData.StatusData.IntRight.Text), React.createElement('span', { className: 'text-xs mt-1' }, 'Int Rt') ), - React.createElement('div', { className: 'flex-1 flex flex-col items-center' }, + React.createElement('div', { className: 'flex flex-col items-center sm:flex-1' }, React.createElement('span', { className: `badge ${detailedData.StatusData.Strokes.Color} w-full text-center` }, detailedData.StatusData.Strokes.Text), React.createElement('span', { className: 'text-xs mt-1' }, 'Strokes') ), - React.createElement('div', { className: 'flex-1 flex flex-col items-center' }, + React.createElement('div', { className: 'flex flex-col items-center sm:flex-1' }, React.createElement('span', { className: `badge ${detailedData.StatusData.BytesSent.Color} w-full text-center` }, detailedData.StatusData.BytesSent.Text), React.createElement('span', { className: 'text-xs mt-1' }, 'Sent') ), - React.createElement('div', { className: 'flex-1 flex flex-col items-center' }, + React.createElement('div', { className: 'flex flex-col items-center sm:flex-1' }, React.createElement('span', { className: `badge ${detailedData.StatusData.BytesRecd.Color} w-full text-center` }, detailedData.StatusData.BytesRecd.Text), React.createElement('span', { className: 'text-xs mt-1' }, 'MB Rec') ), - React.createElement('div', { className: 'flex-1 flex flex-col items-center' }, + React.createElement('div', { className: 'flex flex-col items-center sm:flex-1' }, React.createElement('span', { className: `badge ${detailedData.StatusData.CANStatus.Color} w-full text-center` }, detailedData.StatusData.CANStatus.Text), React.createElement('span', { className: 'text-xs mt-1' }, 'CAN') ), - React.createElement('div', { className: 'flex-1 flex flex-col items-center' }, + React.createElement('div', { className: 'flex flex-col items-center sm:flex-1' }, React.createElement('span', { className: 'badge w-full text-center', style: { @@ -517,7 +530,7 @@ function CombinedDashboard() { React.createElement('div', { className: 'card-body py-1 px-1.5' }, React.createElement('h2', { className: 'card-title opacity-80' }, 'Left Heart'), - React.createElement('div', { className: 'grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4' }, + React.createElement('div', { className: 'grid grid-cols-2 md:grid-cols-4 sm:grid-cols-4 lg:grid-cols-4 gap-4' }, //createDetailCard('Stroke Vol', detailedData.LeftHeart.StrokeVolume, 'stroke.png', 'base-content', detailedData, 'mL'), createStrokeCard('Stroke Len', detailedData.LeftHeart.TargetStrokeLen, detailedData.LeftHeart.ActualStrokeLen, 'piston.png'), @@ -526,11 +539,14 @@ function CombinedDashboard() { //createDetailCard('Atrial Press', detailedData.LeftHeart.AtrialPressure, 'pressure.png', 'base-content', detailedData, 'mmHg'), - createDetailCard('Medical Press', detailedData.LeftHeart.MedicalPressure, 'pressure.png', 'base-content', detailedData, 'mmHg'), + createDetailCard('Med Sensor', detailedData.LeftHeart.MedicalPressure, 'pressure.png', 'base-content', detailedData, 'mmHg'), createDetailCard('Cardiac Out', detailedData.LeftHeart.CardiacOutput, 'cardiacout.png', 'base-content', detailedData, 'L/min'), - createDetailCard('Power', detailedData.LeftHeart.PowerConsumption, 'watts.png', 'base-content', detailedData, 'W') + createDetailCard('Power', detailedData.LeftHeart.PowerConsumption, 'watts.png', 'base-content', detailedData, 'W'), + createDetailCard('Sensor Tmp', detailedData.LeftHeart.SensorTemperature, 'temperature.png', 'base-content', detailedData, '°C'), + createDetailCard('Therm Tmp', detailedData.LeftHeart.ThermistorTemperature, 'temperature.png', 'base-content', detailedData, '°C'), + createDetailCard('CPU Load', detailedData.LeftHeart.CpuLoad, 'cpu.png', 'base-content', detailedData, '%') ) @@ -544,7 +560,7 @@ function CombinedDashboard() { React.createElement('div', { className: 'card-body px-1.5 py-1' }, React.createElement('h2', { className: 'card-title opacity-80' }, 'Right Heart'), - React.createElement('div', { className: 'grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4' }, + React.createElement('div', { className: 'grid grid-cols-2 md:grid-cols-4 sm:grid-cols-4 lg:grid-cols-4 gap-4' }, //createDetailCard('Stroke Vol', detailedData.RightHeart.StrokeVolume, 'stroke.png', 'base-content', detailedData, 'mL'), createStrokeCard('Stroke Len', detailedData.RightHeart.TargetStrokeLen, detailedData.RightHeart.ActualStrokeLen, 'piston.png'), @@ -553,11 +569,14 @@ function CombinedDashboard() { //createDetailCard('Atrial Press', detailedData.RightHeart.AtrialPressure, 'pressure.png', 'base-content', detailedData, 'mmHg'), - createDetailCard('Medical Press', detailedData.RightHeart.MedicalPressure, 'pressure.png', 'base-content', detailedData, 'mmHg'), + createDetailCard('Med Sensor', detailedData.RightHeart.MedicalPressure, 'pressure.png', 'base-content', detailedData, 'mmHg'), createDetailCard('Cardiac Out', detailedData.RightHeart.CardiacOutput, 'cardiacout.png', 'base-content', detailedData, 'L/min'), - createDetailCard('Power', detailedData.RightHeart.PowerConsumption, 'watts.png', 'base-content', detailedData, 'W') + createDetailCard('Power', detailedData.RightHeart.PowerConsumption, 'watts.png', 'base-content', detailedData, 'W'), + createDetailCard('Sensor Tmp', detailedData.RightHeart.SensorTemperature, 'temperature.png', 'base-content', detailedData, '°C'), + createDetailCard('Therm Tmp', detailedData.RightHeart.ThermistorTemperature, 'temperature.png', 'base-content', detailedData, '°C'), + createDetailCard('CPU Load', detailedData.RightHeart.CpuLoad, 'cpu.png', 'base-content', detailedData, '%') ) @@ -571,7 +590,7 @@ function CombinedDashboard() { React.createElement('div', { className: 'card-body px-1.5 py-1' }, React.createElement('h2', { className: 'card-title opacity-80' }, 'System Pressures'), - React.createElement('div', { className: 'grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-5 gap-4' }, + React.createElement('div', { className: 'grid grid-cols-2 md:grid-cols-5 sm:grid-cols-5 lg:grid-cols-5 gap-4' }, createDetailCard('CVP', detailedData.CVPSensor, 'pressure.png', 'base-content', detailedData, 'mmHg'), createDetailCard('PAP', detailedData.PAPSensor, 'pressure.png', 'base-content', detailedData, 'mmHg'),