FastMCP Todo Server

  • dashboard
[ { "id": "mqtt-status-dashboard", "type": "tab", "label": "Device Status Dashboard", "disabled": false, "info": "Dashboard showing device status lights based on MQTT messages" }, { "id": "mqtt-status-in", "type": "mqtt in", "z": "mqtt-status-dashboard", "name": "Device Status", "topic": "status/+/alive", "qos": "2", "datatype": "auto", "broker": "", "nl": false, "rap": true, "rh": 0, "x": 160, "y": 120, "wires": [ [ "mqtt-status-process" ] ] }, { "id": "mqtt-status-process", "type": "function", "z": "mqtt-status-dashboard", "name": "Process Device Status", "func": "// Extract device name from the topic\nconst topicParts = msg.topic.split('/');\nconst deviceName = topicParts[1];\n\n// Set up the payload for the UI element\nlet status = false;\n\n// Check if there's any message (presence indicates alive)\nif (msg.payload !== undefined && msg.payload !== null) {\n // If the message is '0', '0', false, etc., mark as offline\n if (msg.payload === 0 || msg.payload === '0' || msg.payload === 'false' || msg.payload === false) {\n status = false;\n } else {\n // Otherwise, mark as online\n status = true;\n }\n}\n\n// Create an output message for the dashboard element\nreturn {\n topic: deviceName,\n payload: status,\n deviceName: deviceName, // Add device name for the dashboard UI\n timestamp: Date.now() // Add timestamp for timeout checking\n};", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 380, "y": 120, "wires": [ [ "mqtt-status-ui-controller" ] ] }, { "id": "mqtt-status-ui-controller", "type": "function", "z": "mqtt-status-dashboard", "name": "UI Controller", "func": "// Get the dynamic device name\nconst deviceName = msg.deviceName;\n\n// Get current device statuses from flow context\nlet deviceStatuses = flow.get('deviceStatuses') || {};\nlet deviceTimestamps = flow.get('deviceTimestamps') || {};\n\n// Update the status for this device\ndeviceStatuses[deviceName] = msg.payload;\n\n// Store the timestamp for timeout checking\ndeviceTimestamps[deviceName] = msg.timestamp;\n\n// Save updated device data\nflow.set('deviceStatuses', deviceStatuses);\nflow.set('deviceTimestamps', deviceTimestamps);\n\n// Create a message with all device statuses\nreturn { payload: deviceStatuses };\n", "outputs": 1, "noerr": 0, "initialize": "// Initialize the device tracking\nflow.set('deviceStatuses', {});\nflow.set('deviceTimestamps', {});\n", "finalize": "", "libs": [], "x": 610, "y": 120, "wires": [ [ "mqtt-status-ui-template" ] ] }, { "id": "mqtt-status-ui-template", "type": "ui_template", "z": "mqtt-status-dashboard", "group": "device_status_group", "name": "Dynamic Status Panel", "order": 0, "width": "12", "height": "20", "format": "<div ng-init=\"init()\" id=\"device-status-panel\">\n <h2>Device Status</h2>\n <div class=\"device-list\">\n <div class=\"device-status\" ng-repeat=\"(deviceName, isOnline) in deviceStatuses\">\n <div class=\"status-light\" ng-class=\"{'online': isOnline, 'offline': !isOnline}\"></div>\n <div class=\"device-name\">{{deviceName}}</div>\n </div>\n </div>\n \n <div class=\"large-status-container\">\n <div class=\"large-status\" ng-repeat=\"(deviceName, isOnline) in deviceStatuses\" ng-if=\"isOnline\">\n <div class=\"large-status-light online\"></div>\n <div class=\"large-device-name\">{{deviceName}}</div>\n </div>\n </div>\n</div>\n\n<style>\n #device-status-panel {\n padding: 20px;\n font-family: Arial, sans-serif;\n height: auto;\n overflow: visible;\n }\n \n h2 {\n margin-top: 0;\n margin-bottom: 20px;\n font-size: 28px;\n }\n \n .device-list {\n display: flex;\n flex-direction: column;\n gap: 15px;\n margin-bottom: 30px;\n border-bottom: 1px solid #ccc;\n padding-bottom: 25px;\n }\n \n .device-status {\n display: flex;\n align-items: center;\n gap: 15px;\n }\n \n .status-light {\n width: 22px;\n height: 22px;\n border-radius: 50%;\n box-shadow: 0 0 5px rgba(0,0,0,0.3);\n }\n \n .large-status-container {\n display: flex;\n flex-wrap: wrap;\n justify-content: space-around;\n gap: 30px;\n padding: 10px;\n }\n \n .large-status {\n display: flex;\n flex-direction: column;\n align-items: center;\n margin: 20px 0;\n }\n \n .large-status-light {\n width: 120px;\n height: 120px;\n border-radius: 50%;\n margin-bottom: 15px;\n }\n \n .online {\n background-color: #00ff00;\n box-shadow: 0 0 20px #00ff00;\n }\n \n .offline {\n background-color: #ff0000;\n }\n \n .device-name {\n font-weight: bold;\n font-size: 16px;\n }\n \n .large-device-name {\n font-size: 22px;\n font-weight: bold;\n }\n</style>\n\n<script>\n(function(scope) {\n scope.init = function() {\n scope.deviceStatuses = {};\n \n scope.$watch('msg.payload', function(payload) {\n if (!payload) return;\n \n // Update all device statuses\n scope.deviceStatuses = payload;\n });\n };\n})(scope);\n</script>", "storeOutMessages": true, "fwdInMessages": true, "resendOnRefresh": true, "templateScope": "local", "className": "", "x": 860, "y": 120, "wires": [ [] ] }, { "id": "device-timeout-check", "type": "inject", "z": "mqtt-status-dashboard", "name": "Check Device Timeouts (Every 5 min)", "props": [ { "p": "payload" } ], "repeat": "300", "crontab": "", "once": true, "onceDelay": "10", "topic": "", "payload": "", "payloadType": "date", "x": 200, "y": 200, "wires": [ [ "device-timeout-processor" ] ] }, { "id": "device-timeout-processor", "type": "function", "z": "mqtt-status-dashboard", "name": "Process Timeouts", "func": "// Get the current stored data\nlet deviceStatuses = flow.get('deviceStatuses') || {};\nlet deviceTimestamps = flow.get('deviceTimestamps') || {};\n\n// Current time\nconst now = Date.now();\n\n// One hour in milliseconds\nconst oneHour = 60 * 60 * 1000;\n\n// Check each device\nlet updated = false;\n\nObject.keys(deviceTimestamps).forEach(deviceName => {\n const lastSeen = deviceTimestamps[deviceName];\n \n // If device has been inactive for more than an hour, mark it offline\n if (now - lastSeen > oneHour && deviceStatuses[deviceName] === true) {\n deviceStatuses[deviceName] = false;\n updated = true;\n node.log(`Device ${deviceName} marked offline due to inactivity`);\n }\n});\n\n// Save updated statuses\nflow.set('deviceStatuses', deviceStatuses);\n\n// Only send update if something changed\nif (updated) {\n return { payload: deviceStatuses };\n}\n\nreturn null;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 470, "y": 200, "wires": [ [ "mqtt-status-ui-template" ] ] }, { "id": "mqtt-message-log-in", "type": "mqtt in", "z": "mqtt-status-dashboard", "name": "All MQTT Messages", "topic": "#", "qos": "2", "datatype": "auto", "broker": "", "nl": false, "rap": true, "rh": 0, "x": 170, "y": 280, "wires": [ [ "mqtt-message-log-processor" ] ] }, { "id": "mqtt-message-log-processor", "type": "function", "z": "mqtt-status-dashboard", "name": "Format Message Log", "func": "// Get current message log from flow context (or initialize if not exists)\nlet messageLog = flow.get('messageLog') || [];\n\n// Format the incoming message\nconst timestamp = new Date().toISOString();\nconst topic = msg.topic;\n\n// Format the payload based on type\nlet payload;\nif (typeof msg.payload === 'object') {\n try {\n payload = JSON.stringify(msg.payload);\n } catch (e) {\n payload = '[Object]';\n }\n} else {\n payload = String(msg.payload);\n}\n\n// Create a formatted log entry\nconst logEntry = {\n timestamp: timestamp,\n topic: topic,\n payload: payload\n};\n\n// Add to the beginning of the log array (newest first)\nmessageLog.unshift(logEntry);\n\n// Limit log size to prevent memory issues (keep last 100 messages)\nif (messageLog.length > 100) {\n messageLog = messageLog.slice(0, 100);\n}\n\n// Store updated log\nflow.set('messageLog', messageLog);\n\n// Return the entire log for the UI\nreturn { payload: messageLog };\n", "outputs": 1, "noerr": 0, "initialize": "// Initialize empty message log\nflow.set('messageLog', []);\n", "finalize": "", "libs": [], "x": 430, "y": 280, "wires": [ [ "mqtt-message-log-ui-template" ] ] }, { "id": "mqtt-message-log-ui-template", "type": "ui_template", "z": "mqtt-status-dashboard", "group": "mqtt_log_group", "name": "MQTT Message Log", "order": 0, "width": "12", "height": "12", "format": "<div ng-init=\"init()\" id=\"mqtt-log-panel\">\n <h2>MQTT Message Log</h2>\n \n <div class=\"log-controls\">\n <input type=\"text\" ng-model=\"filterText\" placeholder=\"Filter by topic...\" />\n <button ng-click=\"clearLog()\">Clear Log</button>\n </div>\n \n <div class=\"mqtt-log-container\">\n <table class=\"mqtt-log-table\">\n <thead>\n <tr>\n <th>Time</th>\n <th>Topic</th>\n <th>Payload</th>\n </tr>\n </thead>\n <tbody>\n <tr ng-repeat=\"entry in messageLog | filter: topicFilter\" class=\"log-entry\">\n <td class=\"timestamp\">{{formatTime(entry.timestamp)}}</td>\n <td class=\"topic\">{{entry.topic}}</td>\n <td class=\"payload\">{{entry.payload}}</td>\n </tr>\n </tbody>\n </table>\n </div>\n</div>\n\n<style>\n #mqtt-log-panel {\n padding: 20px;\n font-family: Arial, sans-serif;\n }\n \n h2 {\n margin-top: 0;\n margin-bottom: 20px;\n font-size: 28px;\n }\n \n .log-controls {\n display: flex;\n margin-bottom: 15px;\n gap: 10px;\n }\n \n .log-controls input {\n flex-grow: 1;\n padding: 8px;\n border: 1px solid #ccc;\n border-radius: 4px;\n }\n \n .log-controls button {\n padding: 8px 15px;\n background-color: #f2f2f2;\n border: 1px solid #ccc;\n border-radius: 4px;\n cursor: pointer;\n }\n \n .log-controls button:hover {\n background-color: #e6e6e6;\n }\n \n .mqtt-log-container {\n height: 300px;\n overflow-y: auto;\n border: 1px solid #ccc;\n border-radius: 4px;\n background-color: #f9f9f9;\n }\n \n .mqtt-log-table {\n width: 100%;\n border-collapse: collapse;\n }\n \n .mqtt-log-table th {\n position: sticky;\n top: 0;\n background-color: #e0e0e0;\n padding: 10px;\n text-align: left;\n font-weight: bold;\n border-bottom: 2px solid #ccc;\n }\n \n .mqtt-log-table td {\n padding: 8px;\n border-bottom: 1px solid #ddd;\n word-break: break-word;\n }\n \n .log-entry:hover {\n background-color: #f0f0f0;\n }\n \n .timestamp {\n width: 180px;\n white-space: nowrap;\n }\n \n .topic {\n width: 30%;\n }\n \n .payload {\n width: 40%;\n }\n</style>\n\n<script>\n(function(scope) {\n scope.init = function() {\n scope.messageLog = [];\n scope.filterText = '';\n \n scope.topicFilter = function(item) {\n if (!scope.filterText) return true;\n return item.topic.toLowerCase().includes(scope.filterText.toLowerCase());\n };\n \n scope.formatTime = function(timestamp) {\n // Format ISO timestamp to more readable format\n const date = new Date(timestamp);\n return date.toLocaleTimeString() + '.' + date.getMilliseconds().toString().padStart(3, '0');\n };\n \n scope.clearLog = function() {\n scope.messageLog = [];\n // Use $injector to get the $http service\n const $http = this.$injector.get('$http');\n // Send a message to Node-RED to clear the log\n $http.post('ui/mqtt-message-log-ui-template/clearLog', {});\n };\n \n scope.$watch('msg.payload', function(payload) {\n if (!payload) return;\n scope.messageLog = payload;\n });\n };\n})(scope);\n</script>", "storeOutMessages": true, "fwdInMessages": true, "resendOnRefresh": true, "templateScope": "local", "className": "", "x": 680, "y": 280, "wires": [ [] ] }, { "id": "mqtt-message-log-clear", "type": "ui_ui_control", "z": "mqtt-status-dashboard", "name": "Handle Clear Log", "events": "mqtt-message-log-ui-template:clearLog", "x": 430, "y": 340, "wires": [ [ "mqtt-message-log-clear-function" ] ] }, { "id": "mqtt-message-log-clear-function", "type": "function", "z": "mqtt-status-dashboard", "name": "Clear Message Log", "func": "// Clear the message log in flow context\nflow.set('messageLog', []);\n\n// Return empty array to update the UI\nreturn { payload: [] };\n", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 670, "y": 340, "wires": [ [ "mqtt-message-log-ui-template" ] ] }, { "id": "device_status_group", "type": "ui_group", "name": "Device Status", "tab": "device_status_tab", "order": 1, "disp": true, "width": "12", "collapse": false }, { "id": "mqtt_log_group", "type": "ui_group", "name": "MQTT Message Log", "tab": "device_status_tab", "order": 2, "disp": true, "width": "12", "collapse": false }, { "id": "device_status_tab", "type": "ui_tab", "name": "Device Status Dashboard", "icon": "dashboard", "disabled": false, "hidden": false } ]