Merge pull request #45 from beatz174-bit/codex/update-node-red-flow-for-idempotency

Make Grafana Docker Safe Update flow idempotent for repeated alerts
This commit is contained in:
beatz174-bit
2026-04-15 07:42:34 +10:00
committed by GitHub
+6 -6
View File
@@ -355,7 +355,7 @@
"type": "function", "type": "function",
"z": "00b02bbd01c91485", "z": "00b02bbd01c91485",
"name": "Deployment Success", "name": "Deployment Success",
"func": "const attempts = flow.get(\"dockerUpdateAttempts\") || {};\n\nif (msg.updateKey && attempts[msg.updateKey]) {\n delete attempts[msg.updateKey];\n flow.set(\"dockerUpdateAttempts\", attempts);\n}\nnode.log(`Deployment Successful\n Container: ${msg.container}\n Image: ${msg.image}\n Host: ${msg.host}\n`);\n\nreturn msg;", "func": "let attempts = flow.get(\"dockerUpdateAttempts\") || {};\n\nif (msg.updateKey) {\n const existing = attempts[msg.updateKey] || {};\n attempts[msg.updateKey] = {\n ...existing,\n time: existing.time || Date.now(),\n status: \"success\",\n completedAt: Date.now(),\n notified: true\n };\n flow.set(\"dockerUpdateAttempts\", attempts);\n}\n\nnode.log(`Deployment Successful\n Container: ${msg.container}\n Image: ${msg.image}\n Host: ${msg.host}\n`);\n\nreturn msg;",
"outputs": 1, "outputs": 1,
"timeout": "", "timeout": "",
"noerr": 0, "noerr": 0,
@@ -492,8 +492,8 @@
"id": "f4b91e278109e661", "id": "f4b91e278109e661",
"type": "function", "type": "function",
"z": "00b02bbd01c91485", "z": "00b02bbd01c91485",
"name": "Repeat update Suppresed", "name": "Repeat update Suppressed",
"func": "const labels = msg.payload.alerts.labels || {};\n\nnode.warn(\n `${labels.container || \"unknown\"} ` +\n `(${labels.compose_image || labels.running_image || \"unknown image\"})`\n);\nreturn msg;", "func": "const labels = msg.payload.alerts.labels || {};\nconst container = labels.container || \"unknown\";\nconst image = labels.compose_image || labels.running_image || labels.image || \"unknown image\";\nconst status = (msg.attemptInfo && msg.attemptInfo.status) || \"unknown\";\n\nnode.warn(\n `Repeated update suppressed for ${container} (${image}). Prior status: ${status}`\n);\n\nreturn null;",
"outputs": 1, "outputs": 1,
"timeout": "", "timeout": "",
"noerr": 0, "noerr": 0,
@@ -565,7 +565,7 @@
"type": "function", "type": "function",
"z": "00b02bbd01c91485", "z": "00b02bbd01c91485",
"name": "Check Already Attempted", "name": "Check Already Attempted",
"func": "const labels = msg.payload.alerts.labels || {};\n\nconst container = labels.container;\nconst image = labels.compose_image || labels.running_image || labels.image;\nconst project = labels.com_docker_compose_project;\n\nif (!container || !image) {\n node.warn(\"Missing container/image labels; skipping update attempt tracking\");\n return [null, null];\n}\n\n// Load persistent attempt registry\nlet attempts = flow.get(\"dockerUpdateAttempts\") || {};\n\nconst host = project === \"core\" ? \"docker\" : \"raspi\";\n\n// Unique key for this exact update attempt\nconst key = `${container}|${image}|${host}`;\n\n// If we've already tried this image for this container, suppress it\nif (attempts[key]) {\n node.warn(\n `Ignoring repeated update alert for ${container} -> ${image}. ` +\n `Already attempted at ${new Date(attempts[key].time).toISOString()}`\n );\n\n msg.suppressed = true;\n msg.updateKey = key;\n msg.attemptInfo = attempts[key];\n\n return [null, msg];\n}\n\n// First time we've seen this update, record it immediately\nattempts[key] = {\n time: Date.now(),\n status: \"started\"\n};\n\nflow.set(\"dockerUpdateAttempts\", attempts);\n\nmsg.updateKey = key;\n\nnode.log(`First attempt for ${container} -> ${image}`);\n\nreturn [msg, null];", "func": "const SUCCESS_TTL_MS = 36 * 60 * 60 * 1000;\nconst now = Date.now();\n\nconst labels = msg.payload.alerts.labels || {};\n\nconst container = labels.container;\nconst image = labels.compose_image || labels.running_image || labels.image;\nconst project = labels.com_docker_compose_project;\n\nif (!container || !image) {\n node.warn(\"Missing container/image labels; skipping update attempt tracking\");\n return [null, null];\n}\n\n// Load persistent attempt registry\nlet attempts = flow.get(\"dockerUpdateAttempts\") || {};\n\n// Prune old successful records so they don't accumulate forever\nfor (const [k, v] of Object.entries(attempts)) {\n if (v && v.status === \"success\" && v.completedAt && (now - v.completedAt) > SUCCESS_TTL_MS) {\n delete attempts[k];\n }\n}\n\nconst host = project === \"core\" ? \"docker\" : \"raspi\";\n\n// Unique key for this exact update attempt\nconst key = `${container}|${image}|${host}`;\n\n// If we've already tried this image for this container, suppress it\nif (attempts[key]) {\n node.warn(\n `Ignoring repeated update alert for ${container} -> ${image}. ` +\n `Already handled with status '${attempts[key].status}' at ${new Date(attempts[key].time).toISOString()}`\n );\n\n msg.suppressed = true;\n msg.updateKey = key;\n msg.attemptInfo = attempts[key];\n\n flow.set(\"dockerUpdateAttempts\", attempts);\n\n return [null, msg];\n}\n\n// First time we've seen this update, record it immediately\nattempts[key] = {\n time: Date.now(),\n status: \"started\"\n};\n\nflow.set(\"dockerUpdateAttempts\", attempts);\n\nmsg.updateKey = key;\n\nnode.log(`First attempt for ${container} -> ${image}`);\n\nreturn [msg, null];",
"outputs": 2, "outputs": 2,
"timeout": 0, "timeout": 0,
"noerr": 0, "noerr": 0,
@@ -632,7 +632,7 @@
"type": "function", "type": "function",
"z": "00b02bbd01c91485", "z": "00b02bbd01c91485",
"name": "get docker update lockouts", "name": "get docker update lockouts",
"func": "const attempts = flow.get(\"dockerUpdateAttempts\") || {};\n\nconst output = Object.entries(attempts).map(([key, value]) => ({\n key,\n status: value.status,\n firstAttempt: new Date(value.time).toISOString(),\n failedAt: value.failedAt\n ? new Date(value.failedAt).toISOString()\n : null,\n notified: !!value.notified\n}));\n\nmsg.payload = output;\nnode.log(JSON.stringify(msg.payload, null, 2));\nreturn msg;", "func": "const attempts = flow.get(\"dockerUpdateAttempts\") || {};\n\nconst output = Object.entries(attempts).map(([key, value]) => ({\n key,\n status: value.status || null,\n firstAttempt: value.time ? new Date(value.time).toISOString() : null,\n completedAt: value.completedAt ? new Date(value.completedAt).toISOString() : null,\n failedAt: value.failedAt ? new Date(value.failedAt).toISOString() : null,\n notified: !!value.notified\n}));\n\nmsg.payload = output;\nnode.log(JSON.stringify(msg.payload, null, 2));\nreturn msg;",
"outputs": 1, "outputs": 1,
"timeout": 0, "timeout": 0,
"noerr": 0, "noerr": 0,
@@ -869,7 +869,7 @@
"type": "function", "type": "function",
"z": "c5240b64a962ea54", "z": "c5240b64a962ea54",
"name": "Domain name alerts", "name": "Domain name alerts",
"func": "const name = msg.payload.monitor.name;\nconst hb = msg.payload.heartbeat;\n\nconst isUp = hb.status === 1;\n\nconst icon = isUp ? \"\" : \"🔴\";\nconst state = isUp ? \"UP\" : \"DOWN\";\n\n// Build a short title for the Gotify notification\nconst title = `${icon} ${name} is ${state}`;\nconst time = hb.localDateTime.split(\" \")[1].split(\":\").slice(0, 2).join(\":\");\n// Build a more detailed message\nlet message = `${icon} Monitor: ${name}\\n`;\nmessage += `Status: ${state}\\n`;\nmessage += `Time: ${time}\\n`;\nmessage += `Details: ${hb.msg}`;\n\nif (hb.ping !== undefined) {\n message += `\\nPing: ${hb.ping} ms`;\n}\n\nif (hb.duration !== undefined) {\n message += `\\nCheck Interval: ${hb.duration} sec`;\n}\n\n// Higher priority for down alerts\nmsg.payload = {\n title: title,\n message: message,\n priority: isUp ? 5 : 8\n};\n\nreturn msg;", "func": "const name = msg.payload.monitor.name;\nconst hb = msg.payload.heartbeat;\n\nconst isUp = hb.status === 1;\n\nconst icon = isUp ? \"\u2705\" : \"\ud83d\udd34\";\nconst state = isUp ? \"UP\" : \"DOWN\";\n\n// Build a short title for the Gotify notification\nconst title = `${icon} ${name} is ${state}`;\nconst time = hb.localDateTime.split(\" \")[1].split(\":\").slice(0, 2).join(\":\");\n// Build a more detailed message\nlet message = `${icon} Monitor: ${name}\\n`;\nmessage += `Status: ${state}\\n`;\nmessage += `Time: ${time}\\n`;\nmessage += `Details: ${hb.msg}`;\n\nif (hb.ping !== undefined) {\n message += `\\nPing: ${hb.ping} ms`;\n}\n\nif (hb.duration !== undefined) {\n message += `\\nCheck Interval: ${hb.duration} sec`;\n}\n\n// Higher priority for down alerts\nmsg.payload = {\n title: title,\n message: message,\n priority: isUp ? 5 : 8\n};\n\nreturn msg;",
"outputs": 1, "outputs": 1,
"timeout": 0, "timeout": 0,
"noerr": 0, "noerr": 0,