[ { "id": "00b02bbd01c91485", "type": "tab", "label": "Grafana Docker Safe Update", "disabled": false, "info": "" }, { "id": "f16653fb4cb05bbe", "type": "tab", "label": "Health Check", "disabled": false, "info": "", "env": [] }, { "id": "d0f45d2b6f117365", "type": "tab", "label": "uptime-kuma alerts", "disabled": false, "info": "", "env": [] }, { "id": "c5240b64a962ea54", "type": "tab", "label": "Gotify Messages", "disabled": false, "info": "", "env": [] }, { "id": "22f648a14d2e8fae", "type": "inject", "z": "00b02bbd01c91485", "name": "Test Alert", "props": [ { "p": "payload" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": "", "topic": "", "payload": "{\"receiver\":\"node-red\",\"status\":\"firing\",\"alerts\":[{\"status\":\"firing\",\"labels\":{\"Active\":\"telegraf: 0 updates\",\"Restored\":\"telegraf: 0 updates\",\"alertname\":\"Updates\",\"com_docker_compose_project\":\"core\",\"compose_image\":\"telegraf:latest\",\"container\":\"telegraf\",\"grafana_folder\":\"Infrastructure\",\"job\":\"container-updates\",\"running_image\":\"telegraf:latest\"},\"annotations\":{},\"startsAt\":\"2026-03-31T01:16:10Z\",\"endsAt\":\"0001-01-01T00:00:00Z\",\"generatorURL\":\"https://grafana.lan.ddnsgeek.com/alerting/grafana/bfgysfo3iregwf/view?orgId=1\",\"fingerprint\":\"49b1e1dbf1255191\",\"silenceURL\":\"https://grafana.lan.ddnsgeek.com/alerting/silence/new?alertmanager=grafana&matcher=__alert_rule_uid__%3Dbfgysfo3iregwf&matcher=Active%3Dtelegraf%3A+0+updates&matcher=Restored%3Dtelegraf%3A+0+updates&matcher=com_docker_compose_project%3Dcore&matcher=compose_image%3Dtelegraf%3Alatest&matcher=container%3Dtelegraf&matcher=job%3Dcontainer-updates&matcher=running_image%3Dtelegraf%3Alatest&orgId=1\",\"dashboardURL\":\"https://grafana.lan.ddnsgeek.com/d/ad895wr?from=1774916170000&orgId=1&to=1774926701171\",\"panelURL\":\"https://grafana.lan.ddnsgeek.com/d/ad895wr?from=1774916170000&orgId=1&to=1774926701171&viewPanel=6\",\"ruleUID\":\"bfgysfo3iregwf\",\"values\":{\"A\":1,\"D\":1},\"valueString\":\"[ var='A' labels={com_docker_compose_project=core, compose_image=telegraf:latest, container=telegraf, job=container-updates, running_image=telegraf:latest} type='query' value=1 ], [ var='D' labels={com_docker_compose_project=core, compose_image=telegraf:latest, container=telegraf, job=container-updates, running_image=telegraf:latest} type='threshold' value=1 ]\",\"orgId\":1},{\"status\":\"firing\",\"labels\":{\"Active\":\"update-test: 0 updates\",\"Restored\":\"update-test: 0 updates\",\"alertname\":\"Updates\",\"com_docker_compose_project\":\"core\",\"compose_image\":\"nginx:1.27.1\",\"container\":\"update-test\",\"grafana_folder\":\"Infrastructure\",\"job\":\"container-updates\",\"running_image\":\"nginx:1.27.0\"},\"annotations\":{},\"startsAt\":\"2026-03-31T01:15:10Z\",\"endsAt\":\"0001-01-01T00:00:00Z\",\"generatorURL\":\"https://grafana.lan.ddnsgeek.com/alerting/grafana/bfgysfo3iregwf/view?orgId=1\",\"fingerprint\":\"c56bd9f28943cfac\",\"silenceURL\":\"https://grafana.lan.ddnsgeek.com/alerting/silence/new?alertmanager=grafana&matcher=__alert_rule_uid__%3Dbfgysfo3iregwf&matcher=Active%3Dupdate-test%3A+0+updates&matcher=Restored%3Dupdate-test%3A+0+updates&matcher=com_docker_compose_project%3Dcore&matcher=compose_image%3Dnginx%3A1.27.1&matcher=container%3Dupdate-test&matcher=job%3Dcontainer-updates&matcher=running_image%3Dnginx%3A1.27.0&orgId=1\",\"dashboardURL\":\"https://grafana.lan.ddnsgeek.com/d/ad895wr?from=1774916110000&orgId=1&to=1774926701171\",\"panelURL\":\"https://grafana.lan.ddnsgeek.com/d/ad895wr?from=1774916110000&orgId=1&to=1774926701171&viewPanel=6\",\"ruleUID\":\"bfgysfo3iregwf\",\"values\":{\"A\":1,\"D\":1},\"valueString\":\"[ var='A' labels={com_docker_compose_project=core, compose_image=nginx:1.27.1, container=update-test, job=container-updates, running_image=nginx:1.27.0} type='query' value=1 ], [ var='D' labels={com_docker_compose_project=core, compose_image=nginx:1.27.1, container=update-test, job=container-updates, running_image=nginx:1.27.0} type='threshold' value=1 ]\",\"orgId\":1}],\"groupLabels\":{\"alertname\":\"Updates\",\"grafana_folder\":\"Infrastructure\"},\"commonLabels\":{\"alertname\":\"Updates\",\"com_docker_compose_project\":\"core\",\"grafana_folder\":\"Infrastructure\",\"job\":\"container-updates\"},\"commonAnnotations\":{},\"externalURL\":\"https://grafana.lan.ddnsgeek.com/\",\"appVersion\":\"12.4.2\",\"version\":\"1\",\"groupKey\":\"{}/{__grafana_autogenerated__=\\\"true\\\"}/{__grafana_receiver__=\\\"node-red\\\"}/{__grafana_route_settings_hash__=\\\"f177b219cbbdd2e2\\\"}:{alertname=\\\"Updates\\\", grafana_folder=\\\"Infrastructure\\\"}\",\"truncatedAlerts\":0,\"orgId\":1,\"title\":\"[FIRING:2] Updates Infrastructure (core container-updates)\",\"state\":\"alerting\",\"message\":\"**Firing**\\n\\nValue: A=1, D=1\\nLabels:\\n - alertname = Updates\\n - Active = telegraf: 0 updates\\n - Restored = telegraf: 0 updates\\n - com_docker_compose_project = core\\n - compose_image = telegraf:latest\\n - container = telegraf\\n - grafana_folder = Infrastructure\\n - job = container-updates\\n - running_image = telegraf:latest\\nAnnotations:\\nSource: https://grafana.lan.ddnsgeek.com/alerting/grafana/bfgysfo3iregwf/view?orgId=1\\nSilence: https://grafana.lan.ddnsgeek.com/alerting/silence/new?alertmanager=grafana&matcher=__alert_rule_uid__%3Dbfgysfo3iregwf&matcher=Active%3Dtelegraf%3A+0+updates&matcher=Restored%3Dtelegraf%3A+0+updates&matcher=com_docker_compose_project%3Dcore&matcher=compose_image%3Dtelegraf%3Alatest&matcher=container%3Dtelegraf&matcher=job%3Dcontainer-updates&matcher=running_image%3Dtelegraf%3Alatest&orgId=1\\nDashboard: https://grafana.lan.ddnsgeek.com/d/ad895wr?from=1774916170000&orgId=1&to=1774926701171\\nPanel: https://grafana.lan.ddnsgeek.com/d/ad895wr?from=1774916170000&orgId=1&to=1774926701171&viewPanel=6\\n\\nValue: A=1, D=1\\nLabels:\\n - alertname = Updates\\n - Active = update-test: 0 updates\\n - Restored = update-test: 0 updates\\n - com_docker_compose_project = core\\n - compose_image = nginx:1.27.1\\n - container = update-test\\n - grafana_folder = Infrastructure\\n - job = container-updates\\n - running_image = nginx:1.27.0\\nAnnotations:\\nSource: https://grafana.lan.ddnsgeek.com/alerting/grafana/bfgysfo3iregwf/view?orgId=1\\nSilence: https://grafana.lan.ddnsgeek.com/alerting/silence/new?alertmanager=grafana&matcher=__alert_rule_uid__%3Dbfgysfo3iregwf&matcher=Active%3Dupdate-test%3A+0+updates&matcher=Restored%3Dupdate-test%3A+0+updates&matcher=com_docker_compose_project%3Dcore&matcher=compose_image%3Dnginx%3A1.27.1&matcher=container%3Dupdate-test&matcher=job%3Dcontainer-updates&matcher=running_image%3Dnginx%3A1.27.0&orgId=1\\nDashboard: https://grafana.lan.ddnsgeek.com/d/ad895wr?from=1774916110000&orgId=1&to=1774926701171\\nPanel: https://grafana.lan.ddnsgeek.com/d/ad895wr?from=1774916110000&orgId=1&to=1774926701171&viewPanel=6\\n\"}", "payloadType": "json", "x": 140, "y": 100, "wires": [ [ "671243b648c669fe" ] ] }, { "id": "17eef927bdc12e54", "type": "function", "z": "00b02bbd01c91485", "name": "Create Pull Command", "func": "const labels = msg.payload.alerts.labels || {};\nconst container = labels.container;\nconst image = labels.compose_image || labels.running_image || labels.image;\nconst project = labels.com_docker_compose_project;\n\nconst host = project === \"core\" ? \"docker\" : \"raspi\";\n\nif (!container) {\n node.warn(\"No container found in alert labels\");\n return null;\n}\n\nmsg.payload = `PROJECT_ROOT=\"/compose/${host}\" /compose/${host}/services-up.sh --profile all pull -q ${container}`;\nmsg.container = container;\nmsg.image = image;\nmsg.host = host;\n\nnode.log(`New docker update available\n Container: ${container}\n Image: ${image}\n Host: ${host}`);\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1300, "y": 80, "wires": [ [ "e92429c3061966f7" ] ] }, { "id": "e92429c3061966f7", "type": "exec", "z": "00b02bbd01c91485", "command": "", "addpay": "payload", "append": "", "useSpawn": "false", "timer": "", "winHide": false, "oldrc": false, "name": "Pull Image", "x": 1510, "y": 80, "wires": [ [ "5c36c5b0e44d7cb0" ], [ "5c36c5b0e44d7cb0" ], [ "3af6aa9cbaad6ff9" ] ] }, { "id": "b8a933687445fb13", "type": "exec", "z": "00b02bbd01c91485", "command": "", "addpay": "payload", "append": "", "useSpawn": "false", "timer": "70", "winHide": false, "oldrc": true, "name": "Test New Image", "x": 2160, "y": 80, "wires": [ [ "40e040d2b58bbaa1", "4c182d7a643805cd" ], [ "4c182d7a643805cd" ], [] ] }, { "id": "40e040d2b58bbaa1", "type": "switch", "z": "00b02bbd01c91485", "name": "Check Test Result", "property": "payload", "propertyType": "msg", "rules": [ { "t": "cont", "v": "0", "vt": "str" }, { "t": "else" } ], "checkall": "true", "repair": false, "outputs": 2, "x": 2370, "y": 80, "wires": [ [ "da5d95a30589805c" ], [ "47ac22574893de41" ] ] }, { "id": "ab2063f9180cd1aa", "type": "exec", "z": "00b02bbd01c91485", "command": "", "addpay": "payload", "append": "", "useSpawn": "false", "timer": "", "winHide": false, "oldrc": true, "name": "Deploy Production Container", "x": 2960, "y": 80, "wires": [ [ "32e3684b63a6f13e" ], [ "e8e4151333935b78", "32e3684b63a6f13e" ], [] ] }, { "id": "a4c73e8e3e87fab7", "type": "http in", "z": "00b02bbd01c91485", "name": "Grafana Webhook", "url": "/grafana-update", "method": "post", "upload": false, "skipBodyParsing": false, "swaggerDoc": "", "x": 110, "y": 60, "wires": [ [ "671243b648c669fe" ] ] }, { "id": "3af6aa9cbaad6ff9", "type": "switch", "z": "00b02bbd01c91485", "name": "image pulled OK?", "property": "payload.code", "propertyType": "msg", "rules": [ { "t": "eq", "v": "0", "vt": "num" }, { "t": "else" } ], "checkall": "true", "repair": false, "outputs": 2, "x": 1710, "y": 80, "wires": [ [ "96c81501330c20e3" ], [ "1de1d1befec88aa4" ] ] }, { "id": "96c81501330c20e3", "type": "function", "z": "00b02bbd01c91485", "name": "Build Test Command", "func": "const container = msg.container;\nconst image = msg.image;\nconst host = msg.host;\nmsg.payload = `/data/test-container.sh ${container} ${host}`;\nmsg.image = image;\nmsg.container = container;\nmsg.host = host\nnode.log(`Pull Successful\n Container: ${container}\n Image: ${msg.image}\n Host: ${host}`\n )\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1940, "y": 80, "wires": [ [ "b8a933687445fb13" ] ] }, { "id": "95e72a493b5b736c", "type": "split", "z": "00b02bbd01c91485", "name": "", "splt": "\\n", "spltType": "str", "arraySplt": 1, "arraySpltType": "len", "stream": true, "addname": "", "property": "payload.alerts", "x": 610, "y": 80, "wires": [ [ "298528baa0f776b4" ] ] }, { "id": "da5d95a30589805c", "type": "function", "z": "00b02bbd01c91485", "name": "Build Deploy Command", "func": "const container = msg.container;\nconst image = msg.image;\nconst host = msg.host;\nmsg.payload = `PROJECT_ROOT=\"/compose/${host}\" /compose/${host}/services-up.sh --profile all up -d ${container}`;\nmsg.image = image;\nmsg.container = container;\nmsg.host = host;\nnode.log(`Test Successful\n Container: ${container}\n Image: ${msg.image}\n Host: ${host}`\n )\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 2670, "y": 80, "wires": [ [ "ab2063f9180cd1aa" ] ] }, { "id": "27ac6d1d2b208f99", "type": "link out", "z": "00b02bbd01c91485", "name": "Docker image pull fail", "mode": "link", "links": [ "ba2c4e239d3b1e1d" ], "x": 2095, "y": 140, "wires": [] }, { "id": "49c52031d95c0638", "type": "link out", "z": "00b02bbd01c91485", "name": "Docker image test fail", "mode": "link", "links": [ "10718f4768834442" ], "x": 2795, "y": 140, "wires": [] }, { "id": "b083fcf71669254e", "type": "link out", "z": "00b02bbd01c91485", "name": "Docker update success", "mode": "link", "links": [ "f5771c9cc8e81779" ], "x": 3415, "y": 80, "wires": [] }, { "id": "671243b648c669fe", "type": "function", "z": "00b02bbd01c91485", "name": "WEBHOOK /grafana-update IN", "func": "node.log(\n \"Payload:\\n\" +\n JSON.stringify(msg.payload, null, 2)\n);\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 390, "y": 80, "wires": [ [ "95e72a493b5b736c" ] ] }, { "id": "e8e4151333935b78", "type": "function", "z": "00b02bbd01c91485", "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;", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 3240, "y": 80, "wires": [ [ "b083fcf71669254e" ] ] }, { "id": "1de1d1befec88aa4", "type": "function", "z": "00b02bbd01c91485", "name": "Pull Image Failed", "func": "let attempts = flow.get(\"dockerUpdateAttempts\") || {};\n\nif (msg.updateKey && attempts[msg.updateKey]) {\n const firstFailure = !attempts[msg.updateKey].notified;\n\n attempts[msg.updateKey].status = \"pull_failed\";\n attempts[msg.updateKey].failedAt = Date.now();\n\n if (firstFailure) {\n attempts[msg.updateKey].notified = true;\n\n msg.notification = {\n title: `Docker update locked out: ${msg.container}`,\n message:\n `Automatic update for ${msg.container} failed.\n\n` +\n `Image: ${msg.image}\n` +\n `Host: ${msg.host}\n` +\n `Result: ${attempts[msg.updateKey].status}\n\n` +\n `Further Grafana alerts for this update will be ignored until manual intervention.`\n };\n\n flow.set(\"dockerUpdateAttempts\", attempts);\n return [msg, msg];\n }\n\n flow.set(\"dockerUpdateAttempts\", attempts);\n}\n\nnode.log(`Pull image failed\n Command: ${msg.payload}\n Container: ${msg.container}\n Image: ${msg.image}\n Host: ${msg.host}`);\nreturn [msg, null];", "outputs": 2, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1930, "y": 140, "wires": [ [ "27ac6d1d2b208f99" ], [ "0e6520dfb81a951b" ] ] }, { "id": "47ac22574893de41", "type": "function", "z": "00b02bbd01c91485", "name": "Test Image Failed", "func": "let attempts = flow.get(\"dockerUpdateAttempts\") || {};\n\nif (msg.updateKey && attempts[msg.updateKey]) {\n const firstFailure = !attempts[msg.updateKey].notified;\n\n attempts[msg.updateKey].status = \"test_failed\";\n attempts[msg.updateKey].failedAt = Date.now();\n\n if (firstFailure) {\n attempts[msg.updateKey].notified = true;\n\n msg.notification = {\n title: `Docker update locked out: ${msg.container}`,\n message:\n `Automatic update for ${msg.container} failed.\n\n` +\n `Image: ${msg.image}\n` +\n `Host: ${msg.host}\n` +\n `Result: ${attempts[msg.updateKey].status}\n\n` +\n `Further Grafana alerts for this update will be ignored until manual intervention.`\n };\n\n flow.set(\"dockerUpdateAttempts\", attempts);\n return [msg, msg];\n }\n\n flow.set(\"dockerUpdateAttempts\", attempts);\n}\n\nnode.log(`Test image failed\n Command: ${msg.payload}\n Container: ${msg.container}\n Image: ${msg.image}\n Host: ${msg.host}`);\nreturn [msg, null];", "outputs": 2, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 2650, "y": 140, "wires": [ [ "49c52031d95c0638" ], [ "77675958f77d2d5f" ] ] }, { "id": "298528baa0f776b4", "type": "switch", "z": "00b02bbd01c91485", "name": "docker project name", "property": "payload.alerts.labels.com_docker_compose_project", "propertyType": "msg", "rules": [ { "t": "eq", "v": "core", "vt": "str" }, { "t": "eq", "v": "raspi", "vt": "str" }, { "t": "else" } ], "checkall": "true", "repair": false, "outputs": 3, "x": 800, "y": 80, "wires": [ [ "bad4d106425b7dd2" ], [ "bad4d106425b7dd2" ], [ "1ab4069c08a68fa3" ] ] }, { "id": "1ab4069c08a68fa3", "type": "function", "z": "00b02bbd01c91485", "name": "Unknown Project", "func": "const payload = (msg.payload && typeof msg.payload === \"object\") ? msg.payload : {};\nconst labels = (payload.labels && typeof payload.labels === \"object\") ? payload.labels : {};\n\nconst container = labels.container || \"unknown container\";\nconst image = labels.compose_image || labels.running_image || labels.image || \"unknown image\";\nconst project = labels.com_docker_compose_project || \"unknown project\";\n\nnode.warn(`Unable to map project name ${project} to host.\n\nUpdates for ${container} (${image}) failed`);\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1290, "y": 200, "wires": [ [ "852d039f540dd0d3" ] ] }, { "id": "852d039f540dd0d3", "type": "link out", "z": "00b02bbd01c91485", "name": "Docker updates fail unknown", "mode": "link", "links": [ "7c62509d505c281b" ], "x": 1455, "y": 200, "wires": [] }, { "id": "f4b91e278109e661", "type": "function", "z": "00b02bbd01c91485", "name": "Repeat update Suppresed", "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;", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1310, "y": 140, "wires": [ [] ] }, { "id": "5c36c5b0e44d7cb0", "type": "function", "z": "00b02bbd01c91485", "name": "Logging", "func": "//const labels = msg.payload.alerts.labels || {};\n//const container = labels.container;\n//const image = labels.compose_image || labels.running_image || labels.image;\n\nif (msg.payload != \"\") {\n node.log(msg.payload)\n}\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1680, "y": 40, "wires": [ [] ] }, { "id": "4c182d7a643805cd", "type": "function", "z": "00b02bbd01c91485", "name": "Logging", "func": "//const labels = msg.payload.alerts.labels || {};\n//const container = labels.container;\n//const image = labels.compose_image || labels.running_image || labels.image;\nif (msg.payload != \"\") {\n node.log(msg.payload)\n}\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 2340, "y": 40, "wires": [ [] ] }, { "id": "32e3684b63a6f13e", "type": "function", "z": "00b02bbd01c91485", "name": "Logging", "func": "//const labels = msg.payload.alerts.labels || {};\n//const container = labels.container;\n//const image = labels.compose_image || labels.running_image || labels.image;\nif (msg.payload != \"\") {\n node.log(msg.payload)\n}\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 3200, "y": 40, "wires": [ [] ] }, { "id": "bad4d106425b7dd2", "type": "function", "z": "00b02bbd01c91485", "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];", "outputs": 2, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1050, "y": 80, "wires": [ [ "17eef927bdc12e54" ], [ "f4b91e278109e661" ] ] }, { "id": "0e6520dfb81a951b", "type": "link out", "z": "00b02bbd01c91485", "name": "Docker image update locked", "mode": "link", "links": [ "b72f58c8b6d6d265" ], "x": 2095, "y": 200, "wires": [] }, { "id": "77675958f77d2d5f", "type": "link out", "z": "00b02bbd01c91485", "name": "Docker image update locked", "mode": "link", "links": [ "b72f58c8b6d6d265" ], "x": 2795, "y": 200, "wires": [] }, { "id": "8c74f9646c655f07", "type": "http in", "z": "00b02bbd01c91485", "name": "/docker-update-lockouts in", "url": "/docker-update-lockouts", "method": "get", "upload": false, "skipBodyParsing": false, "swaggerDoc": "", "x": 250, "y": 340, "wires": [ [ "bf13590833ea0288" ] ] }, { "id": "bf13590833ea0288", "type": "function", "z": "00b02bbd01c91485", "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;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 540, "y": 340, "wires": [ [ "2fc3cf8f165b7537" ] ] }, { "id": "2fc3cf8f165b7537", "type": "http response", "z": "00b02bbd01c91485", "name": "", "statusCode": "", "headers": {}, "x": 770, "y": 340, "wires": [] }, { "id": "81f6db49f4ad354a", "type": "http in", "z": "00b02bbd01c91485", "name": "clear lockout", "url": "/docker-update-lockouts/clear", "method": "post", "upload": false, "skipBodyParsing": false, "swaggerDoc": "", "x": 210, "y": 480, "wires": [ [ "28d736c3d3a4cf14" ] ] }, { "id": "7bae4cc1bd52dfd8", "type": "http response", "z": "00b02bbd01c91485", "name": "", "statusCode": "", "headers": {}, "x": 770, "y": 480, "wires": [] }, { "id": "28d736c3d3a4cf14", "type": "function", "z": "00b02bbd01c91485", "name": "Clear lockout", "func": "let attempts = flow.get(\"dockerUpdateAttempts\") || {};\nconst key = (msg.payload.key || \"\").trim(); // trim whitespace\n\n// optionally log all keys for debugging\nnode.log(\"Available keys: \" + Object.keys(attempts).join(\", \"));\nnode.log(\"Requested key: '\" + key + \"'\");\n\nif (key in attempts) { // use 'in' to avoid false negatives\n delete attempts[key];\n flow.set(\"dockerUpdateAttempts\", attempts);\n\n msg.payload = { success: true, cleared: key };\n node.log(`Cleared update lock for ${key}`);\n} else {\n msg.payload = { success: false, error: `No lock found for '${key}'` };\n}\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 490, "y": 480, "wires": [ [ "7bae4cc1bd52dfd8" ] ] }, { "id": "b113615f19bd2588", "type": "http in", "z": "f16653fb4cb05bbe", "name": "/health in", "url": "/health", "method": "get", "upload": false, "skipBodyParsing": false, "swaggerDoc": "", "x": 380, "y": 220, "wires": [ [ "4bf7d7901301ce7e" ] ] }, { "id": "4bf7d7901301ce7e", "type": "function", "z": "f16653fb4cb05bbe", "name": "health check", "func": "msg.payload = { status: \"ok\" };\nmsg.headers = { \"Content-Type\": \"application/json\" };\nnode.log(msg.payload.status);\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 670, "y": 220, "wires": [ [ "269da1ef164eeb72" ] ] }, { "id": "269da1ef164eeb72", "type": "http response", "z": "f16653fb4cb05bbe", "name": "", "statusCode": "", "headers": {}, "x": 950, "y": 220, "wires": [] }, { "id": "40ed22bfb88a0d78", "type": "http in", "z": "d0f45d2b6f117365", "name": "/uptime-kuma in", "url": "/uptime-kuma", "method": "post", "upload": false, "skipBodyParsing": false, "swaggerDoc": "", "x": 320, "y": 200, "wires": [ [ "46affc0319a44fa9", "ab7d36db8e9316f8" ] ] }, { "id": "4e0b1edf6f60d701", "type": "http response", "z": "d0f45d2b6f117365", "name": "response", "statusCode": "", "headers": {}, "x": 860, "y": 200, "wires": [] }, { "id": "46affc0319a44fa9", "type": "link out", "z": "d0f45d2b6f117365", "name": "gotify domain name alerts", "mode": "link", "links": [ "bed17b50dd678228" ], "x": 615, "y": 120, "wires": [] }, { "id": "ab7d36db8e9316f8", "type": "function", "z": "d0f45d2b6f117365", "name": "WEBHOOK /uptime-kuma IN", "func": "node.log(\n \"Payload:\\n\" +\n JSON.stringify(msg.payload, null, 2)\n);\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 620, "y": 200, "wires": [ [ "4e0b1edf6f60d701" ] ] }, { "id": "0301a3cebd1eb23f", "type": "http request", "z": "c5240b64a962ea54", "name": "Send to gotify", "method": "POST", "ret": "obj", "paytoqs": "ignore", "url": "http://gotify/message?token=ATSMpSKrdNjKXeT", "tls": "", "persist": false, "proxy": "", "insecureHTTPParser": false, "authType": "", "senderr": false, "headers": [ { "keyType": "other", "keyValue": "Content-Type", "valueType": "other", "valueValue": "application/json" } ], "x": 1300, "y": 280, "wires": [ [] ] }, { "id": "bed17b50dd678228", "type": "link in", "z": "c5240b64a962ea54", "name": "Doman name alerts", "links": [ "46affc0319a44fa9" ], "x": 275, "y": 100, "wires": [ [ "6e58de21ded7459e" ] ] }, { "id": "6e58de21ded7459e", "type": "function", "z": "c5240b64a962ea54", "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;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 500, "y": 100, "wires": [ [ "8630c7dfcdbcce50" ] ] }, { "id": "8630c7dfcdbcce50", "type": "function", "z": "c5240b64a962ea54", "name": "GOTIFY alert OUT", "func": "node.log(JSON.stringify(msg.payload, null, 2));\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1070, "y": 280, "wires": [ [ "0301a3cebd1eb23f" ] ] }, { "id": "0135d283b9edfb01", "type": "function", "z": "c5240b64a962ea54", "name": "Docker Pull Failed", "func": "const container = msg.container || \"unknown container\";\nconst code = msg.payload.code;\nconst stderr = flow.get(\"pull_stderr\") || \"Unknown error\";\n\nmsg.payload = {\n title: \"Docker Update Failed\",\n message: `Pull failed for ${container}\\n${stderr}`,\n priority: 8\n};\n\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 490, "y": 200, "wires": [ [ "8630c7dfcdbcce50" ] ] }, { "id": "ba2c4e239d3b1e1d", "type": "link in", "z": "c5240b64a962ea54", "name": "Docker pull failed alerts", "links": [ "27ac6d1d2b208f99" ], "x": 275, "y": 200, "wires": [ [ "c1aa11bb22cc33dd" ] ] }, { "id": "4eafade32c867e40", "type": "function", "z": "c5240b64a962ea54", "name": "Docker test failed", "func": "const container = msg.container || \"unknown container\";\nconst code = msg.payload.code;\nconst stderr = flow.get(\"pull_stderr\") || \"Unknown error\";\n\nmsg.payload = {\n title: \"Testing Failed\",\n message: `Testing failed for ${container}`,\n priority: 8\n};\n\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 490, "y": 300, "wires": [ [ "8630c7dfcdbcce50" ] ] }, { "id": "10718f4768834442", "type": "link in", "z": "c5240b64a962ea54", "name": "Docker test failed alerts", "links": [ "49c52031d95c0638" ], "x": 275, "y": 300, "wires": [ [ "d2aa11bb22cc33dd" ] ] }, { "id": "7d8200040f9b1e83", "type": "function", "z": "c5240b64a962ea54", "name": "Docker update success", "func": "const container = msg.container || \"unknown container\";\nconst code = msg.payload.code;\nconst stderr = flow.get(\"pull_stderr\") || \"Unknown error\";\n\nmsg.payload = {\n title: \"Container Updated\",\n message: `Container: ${container}\n Host: ${msg.host}`,\n priority: 8\n};\n\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 490, "y": 380, "wires": [ [ "8630c7dfcdbcce50" ] ] }, { "id": "f5771c9cc8e81779", "type": "link in", "z": "c5240b64a962ea54", "name": "Docker update success alerts", "links": [ "b083fcf71669254e" ], "x": 275, "y": 380, "wires": [ [ "e3aa11bb22cc33dd" ] ] }, { "id": "c3d07241f4a570af", "type": "function", "z": "c5240b64a962ea54", "name": "Docker updates Unknown Project", "func": "const payload = (msg.payload && typeof msg.payload === \"object\") ? msg.payload : {};\nconst labels = (payload.labels && typeof payload.labels === \"object\") ? payload.labels : {};\n\nconst container = msg.container || labels.container || \"unknown container\";\nconst project = labels.com_docker_compose_project || msg.project || \"unknown project\";\n\nmsg.payload = {\n title: \"Container Updates Failed\",\n message: `The ${container} container has failed.\n\nUnknown project ${project}`,\n priority: 8\n};\n\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 520, "y": 460, "wires": [ [ "8630c7dfcdbcce50" ] ] }, { "id": "7c62509d505c281b", "type": "link in", "z": "c5240b64a962ea54", "name": "Docker update unknown project", "links": [ "852d039f540dd0d3" ], "x": 275, "y": 460, "wires": [ [ "f4aa11bb22cc33dd" ] ] }, { "id": "d1346f7151103832", "type": "function", "z": "c5240b64a962ea54", "name": "Docker updates for Raspi", "func": "const container = msg.container || \"unknown container\";\nconst code = msg.payload.code;\nconst stderr = flow.get(\"pull_stderr\") || \"Unknown error\";\nconst project = msg.payload.labels.com_docker_compose_project\nmsg.payload = {\n title: \"Container Updates Available - Raspi\",\n message: `The ${container} container has updates available.`,\n priority: 8\n};\n\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 490, "y": 520, "wires": [ [ "8630c7dfcdbcce50" ] ] }, { "id": "e6c64cd6d405b8ed", "type": "link in", "z": "c5240b64a962ea54", "name": "Docker update for raspi", "links": [], "x": 275, "y": 520, "wires": [ [ "a5aa11bb22cc33dd" ] ] }, { "id": "b72f58c8b6d6d265", "type": "link in", "z": "c5240b64a962ea54", "name": "Docker update locked", "links": [ "0e6520dfb81a951b", "77675958f77d2d5f" ], "x": 275, "y": 580, "wires": [ [ "b6aa11bb22cc33dd" ] ] }, { "id": "1a9798d5c081240a", "type": "function", "z": "c5240b64a962ea54", "name": "Docker updates locked", "func": "const container = msg.container || \"unknown container\";\nconst defaultPayload = {\n title: \"Update Locked out\",\n message: `Container: ${container}\nHost: ${msg.host}\n\nManual intervention required.`,\n priority: 8\n};\n\nif (msg.notification) {\n msg.payload = {\n title: msg.notification.title || defaultPayload.title,\n message: msg.notification.message || defaultPayload.message,\n priority: 8\n };\n return msg;\n}\n\nmsg.payload = defaultPayload;\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 480, "y": 580, "wires": [ [ "8630c7dfcdbcce50" ] ] }, { "id": "a1f8e9b2c3d4e5f6", "type": "function", "z": "c5240b64a962ea54", "name": "Build update log event", "func": "const nowIso = new Date().toISOString();\nconst startedAt = msg.update_started_at || Date.now();\nconst durationMs = Math.max(0, Date.now() - startedAt);\n\nconst payload = (msg.payload && typeof msg.payload === \"object\") ? msg.payload : {};\nconst labels = payload.labels || {};\n\nconst status = (msg.update_status || payload.status || \"unknown\").toString().toLowerCase();\n\nmsg.payload = JSON.stringify({\n ts: nowIso,\n flow: \"docker-updates\",\n event: msg.update_event || \"attempt\",\n container: msg.container || labels.container || \"unknown\",\n project: labels.com_docker_compose_project || msg.project || \"unknown\",\n host: msg.host || \"unknown\",\n status,\n success: status === \"success\" ? 1 : 0,\n failed: [\"failed\", \"locked\"].includes(status) ? 1 : 0,\n duration_ms: durationMs,\n code: Number.isFinite(Number(payload.code)) ? Number(payload.code) : 0,\n error: (msg.update_error || payload.error || \"\").toString().slice(0, 300)\n});\n\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 690, "y": 660, "wires": [ [ "b1c2d3e4f5a69788" ] ] }, { "id": "b1c2d3e4f5a69788", "type": "file", "z": "c5240b64a962ea54", "name": "Write update event log", "filename": "/data/update-events.ndjson", "filenameType": "str", "appendNewline": true, "createDir": false, "overwriteFile": "false", "encoding": "none", "x": 930, "y": 660, "wires": [ [] ] }, { "id": "c1aa11bb22cc33dd", "type": "function", "z": "c5240b64a962ea54", "name": "Mark Docker Pull Failed", "func": "msg.update_status = \"failed\";\nmsg.update_event = \"completed\";\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 470, "y": 200, "wires": [ [ "0135d283b9edfb01", "a1f8e9b2c3d4e5f6" ] ] }, { "id": "d2aa11bb22cc33dd", "type": "function", "z": "c5240b64a962ea54", "name": "Mark Docker Test Failed", "func": "msg.update_status = \"failed\";\nmsg.update_event = \"completed\";\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 470, "y": 300, "wires": [ [ "4eafade32c867e40", "a1f8e9b2c3d4e5f6" ] ] }, { "id": "e3aa11bb22cc33dd", "type": "function", "z": "c5240b64a962ea54", "name": "Mark Docker Update Success", "func": "msg.update_status = \"success\";\nmsg.update_event = \"completed\";\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 470, "y": 380, "wires": [ [ "7d8200040f9b1e83", "a1f8e9b2c3d4e5f6" ] ] }, { "id": "f4aa11bb22cc33dd", "type": "function", "z": "c5240b64a962ea54", "name": "Mark Docker Unknown Project", "func": "msg.update_status = \"failed\";\nmsg.update_event = \"completed\";\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 470, "y": 460, "wires": [ [ "c3d07241f4a570af", "a1f8e9b2c3d4e5f6" ] ] }, { "id": "a5aa11bb22cc33dd", "type": "function", "z": "c5240b64a962ea54", "name": "Mark Docker Update Attempt", "func": "msg.update_status = \"attempt\";\nmsg.update_event = \"attempt\";\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 470, "y": 520, "wires": [ [ "d1346f7151103832", "a1f8e9b2c3d4e5f6" ] ] }, { "id": "b6aa11bb22cc33dd", "type": "function", "z": "c5240b64a962ea54", "name": "Mark Docker Update Locked", "func": "msg.update_status = \"locked\";\nmsg.update_event = \"completed\";\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 470, "y": 580, "wires": [ [ "1a9798d5c081240a", "a1f8e9b2c3d4e5f6" ] ] } ]