{
  "name": "NFS Mount Watchdog + Auto-Heal v1.0",
  "active": true,
  "isArchived": false,
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 5
            }
          ]
        }
      },
      "id": "8c466a67-a0fa-42dc-8ed8-0918834fc3a7",
      "name": "Every 5 Minutes",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "command": "mountpoint -q /mnt/unraid/data/media  && echo MEDIA_OK  || echo MEDIA_MISSING; mountpoint -q /mnt/unraid/data/photos && echo PHOTOS_OK || echo PHOTOS_MISSING"
      },
      "id": "f5b2fee9-d67a-4da7-80dd-f72e6078aed9",
      "name": "Probe Mounts",
      "type": "n8n-nodes-base.ssh",
      "typeVersion": 1,
      "position": [
        224,
        0
      ],
      "credentials": {
        "sshPrivateKey": {
          "id": "SETUP_REQUIRED",
          "name": "PD SSH"
        }
      },
      "continueOnFail": true
    },
    {
      "parameters": {
        "jsCode": "\nconst out = ($input.first().json.stdout || '') + ($input.first().json.stderr || '');\nconst mediaOk  = out.includes('MEDIA_OK');\nconst photosOk = out.includes('PHOTOS_OK');\nconst missing  = [];\nif (!mediaOk)  missing.push({ mount: '/mnt/unraid/data/media',  label: 'media',\n                               containers: ['plex','audiobookshelf'] });\nif (!photosOk) missing.push({ mount: '/mnt/unraid/data/photos', label: 'photos',\n                               containers: ['immich-server','immich-microservices','immich-machine-learning'] });\nreturn [{ json: { mediaOk, photosOk, anyMissing: missing.length > 0, missing } }];\n"
      },
      "id": "25bb3239-49cb-4570-b706-4022b9aa751c",
      "name": "Parse Mount Status",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        448,
        0
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "442964fa-a29c-4aa2-98d1-9e25e927c496",
              "leftValue": "={{ $json.anyMissing }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equal"
              }
            }
          ],
          "combinator": "and"
        }
      },
      "id": "68b74832-85a7-4193-9382-a0c22a666e66",
      "name": "Any Missing?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        672,
        0
      ]
    },
    {
      "parameters": {
        "command": "sudo bash /mnt/tank/docker/scripts/mount-unraid-nfs.sh 2>&1"
      },
      "id": "b18c7537-0c2f-4122-bfe5-6469d6be6853",
      "name": "Run Mount Script",
      "type": "n8n-nodes-base.ssh",
      "typeVersion": 1,
      "position": [
        896,
        -120
      ],
      "credentials": {
        "sshPrivateKey": {
          "id": "SETUP_REQUIRED",
          "name": "PD SSH"
        }
      },
      "continueOnFail": true
    },
    {
      "parameters": {
        "resume": "timeInterval",
        "unit": "seconds",
        "value": 10
      },
      "id": "2e927f83-9ddc-4816-8ee6-76d21ce21a5e",
      "name": "Wait 10s",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        1120,
        -120
      ],
      "webhookId": "4e89e8d0-7f08-4378-a2b7-63756665b039"
    },
    {
      "parameters": {
        "command": "mountpoint -q /mnt/unraid/data/media  && echo MEDIA_OK  || echo MEDIA_MISSING; mountpoint -q /mnt/unraid/data/photos && echo PHOTOS_OK || echo PHOTOS_MISSING"
      },
      "id": "d15c0c90-e94f-404a-a787-75f1b1b5befa",
      "name": "Re-probe Mounts",
      "type": "n8n-nodes-base.ssh",
      "typeVersion": 1,
      "position": [
        1344,
        -120
      ],
      "credentials": {
        "sshPrivateKey": {
          "id": "SETUP_REQUIRED",
          "name": "PD SSH"
        }
      },
      "continueOnFail": true
    },
    {
      "parameters": {
        "jsCode": "\nconst out = ($input.first().json.stdout || '') + ($input.first().json.stderr || '');\nconst mediaOk  = out.includes('MEDIA_OK');\nconst photosOk = out.includes('PHOTOS_OK');\n\n// What was originally missing (from step 3)\nconst originalMissing = $('Parse Mount Status').first().json.missing || [];\n\n// Which of the originally-missing mounts are now up?\nconst healed = originalMissing.filter(m =>\n  (m.label === 'media'  && mediaOk) ||\n  (m.label === 'photos' && photosOk)\n);\nconst stillDown = originalMissing.filter(m =>\n  (m.label === 'media'  && !mediaOk) ||\n  (m.label === 'photos' && !photosOk)\n);\n\n// Build flat container list to restart (only for healed mounts)\nconst containersToRestart = [...new Set(healed.flatMap(m => m.containers))];\nconst restartCmd = containersToRestart.length > 0\n  ? `sudo docker restart ${containersToRestart.join(' ')}`\n  : 'echo NO_RESTART_NEEDED';\n\nreturn [{ json: {\n  allHealed: stillDown.length === 0,\n  healed, stillDown,\n  containersToRestart,\n  restartCmd,\n  originalMissing\n}}];\n"
      },
      "id": "48de38c2-df08-406f-ad61-809ca6e306a1",
      "name": "Parse Heal Result",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1568,
        -120
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "566dda6e-2234-406a-ad87-c886c7c5ae08",
              "leftValue": "={{ $json.allHealed }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equal"
              }
            }
          ],
          "combinator": "and"
        }
      },
      "id": "29a1b544-97b3-433f-9821-2bf5a8af95d2",
      "name": "Healed?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1792,
        -120
      ]
    },
    {
      "parameters": {
        "command": "={{ $json.restartCmd }}"
      },
      "id": "8d791509-9e18-44c1-bf58-ed3027927710",
      "name": "Restart Containers",
      "type": "n8n-nodes-base.ssh",
      "typeVersion": 1,
      "position": [
        2016,
        -240
      ],
      "credentials": {
        "sshPrivateKey": {
          "id": "SETUP_REQUIRED",
          "name": "PD SSH"
        }
      },
      "continueOnFail": true
    },
    {
      "parameters": {
        "jsCode": "\nconst d = $('Parse Heal Result').first().json;\nconst mountList  = d.healed.map(m => `  \u2022 ${m.mount}`).join('\\n');\nconst containers = d.containersToRestart.join(', ') || 'none';\nconst body = `NFS mounts were missing and have been automatically remounted.\\n\\nRemounted:\\n${mountList}\\n\\nContainers restarted: ${containers}`;\nreturn [{ json: { title: '\ud83d\udd17 NFS Auto-Healed', message: body, priority: 5 } }];\n"
      },
      "id": "75c8976f-d622-43cd-a332-72c32f0c7c60",
      "name": "Format Healed Notify",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2240,
        -240
      ]
    },
    {
      "parameters": {
        "message": "={{ $json.message }}",
        "additionalFields": {
          "priority": "={{ $json.priority }}",
          "title": "={{ $json.title }}"
        },
        "options": {
          "contentType": "text/plain"
        }
      },
      "id": "38705cd2-775b-4ef9-b470-4a7709f132e5",
      "name": "Gotify (Healed)",
      "type": "n8n-nodes-base.gotify",
      "typeVersion": 1,
      "position": [
        2464,
        -240
      ],
      "credentials": {
        "gotifyApi": {
          "id": "SETUP_REQUIRED",
          "name": "Uptime / Infra"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "\nconst d = $('Parse Heal Result').first().json;\nconst downList = d.stillDown.map(m => `  \u2022 ${m.mount}`).join('\\n');\nconst healedList = d.healed.length > 0\n  ? `\\nPartially healed:\\n${d.healed.map(m => '  \u2022 ' + m.mount).join('\\n')}`\n  : '';\nconst body = `NFS mount script ran but the following paths are still unavailable. Manual intervention required.\\n\\nStill down:\\n${downList}${healedList}\\n\\nCheck Serenity (TrueNAS) is up and NFS service is running.`;\nreturn [{ json: { title: '\ud83d\udea8 NFS Down \u2014 Manual Fix Needed', message: body, priority: 9 } }];\n"
      },
      "id": "074b984f-7fc2-45bc-b97f-f5db4d8d589b",
      "name": "Format Escalation",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2016,
        -20
      ]
    },
    {
      "parameters": {
        "message": "={{ $json.message }}",
        "additionalFields": {
          "priority": "={{ $json.priority }}",
          "title": "={{ $json.title }}"
        },
        "options": {
          "contentType": "text/plain"
        }
      },
      "id": "f929dd77-6072-4ac9-9dc4-11246110efed",
      "name": "Gotify (Escalation)",
      "type": "n8n-nodes-base.gotify",
      "typeVersion": 1,
      "position": [
        2240,
        -20
      ],
      "credentials": {
        "gotifyApi": {
          "id": "SETUP_REQUIRED",
          "name": "Uptime / Infra"
        }
      }
    }
  ],
  "connections": {
    "Every 5 Minutes": {
      "main": [
        [
          {
            "node": "Probe Mounts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Probe Mounts": {
      "main": [
        [
          {
            "node": "Parse Mount Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Mount Status": {
      "main": [
        [
          {
            "node": "Any Missing?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Any Missing?": {
      "main": [
        [
          {
            "node": "Run Mount Script",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Run Mount Script": {
      "main": [
        [
          {
            "node": "Wait 10s",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 10s": {
      "main": [
        [
          {
            "node": "Re-probe Mounts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Re-probe Mounts": {
      "main": [
        [
          {
            "node": "Parse Heal Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Heal Result": {
      "main": [
        [
          {
            "node": "Healed?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Healed?": {
      "main": [
        [
          {
            "node": "Restart Containers",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Format Escalation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Restart Containers": {
      "main": [
        [
          {
            "node": "Format Healed Notify",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Healed Notify": {
      "main": [
        [
          {
            "node": "Gotify (Healed)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Escalation": {
      "main": [
        [
          {
            "node": "Gotify (Escalation)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate"
  },
  "staticData": null,
  "meta": null,
  "pinData": {},
  "tags": [
    {
      "name": "monitoring"
    },
    {
      "name": "infrastructure"
    },
    {
      "name": "scheduled"
    }
  ]
}