FastMCP Todo Server
by DanEdens
- 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
}
]