{
  "name": "W09 \u2014 Backup Verification",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "30 3 * * *"
            }
          ]
        }
      },
      "id": "cron-trigger",
      "name": "Cron \u2014 03:30 SAST daily",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        240,
        300
      ]
    },
    {
      "parameters": {
        "resource": "command",
        "command": "velero backup get --output json 2>&1",
        "host": "10.0.10.10",
        "port": 22,
        "username": "kagiso",
        "options": {
          "execTimeout": 15000
        }
      },
      "id": "ssh-velero",
      "name": "SSH \u2014 Velero Backup Status",
      "type": "n8n-nodes-base.ssh",
      "typeVersion": 1,
      "position": [
        460,
        200
      ],
      "credentials": {
        "sshPrivateKey": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "url": "http://kube-prometheus-stack-prometheus.monitoring.svc.cluster.local:9090/api/v1/query",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "query",
              "value": "backup_last_success_timestamp"
            }
          ]
        },
        "options": {
          "timeout": 10000
        }
      },
      "id": "prom-backup-ts",
      "name": "Prometheus \u2014 Backup Timestamps",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        460,
        400
      ]
    },
    {
      "parameters": {
        "url": "http://kube-prometheus-stack-prometheus.monitoring.svc.cluster.local:9090/api/v1/query",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "query",
              "value": "b2_sync_last_status"
            }
          ]
        },
        "options": {
          "timeout": 10000
        }
      },
      "id": "prom-b2",
      "name": "Prometheus \u2014 B2 Sync Status",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        460,
        600
      ]
    },
    {
      "parameters": {
        "jsCode": "const veleroRaw = $('SSH \u2014 Velero Backup Status').first().json.stdout || '';\nconst promTs = $('Prometheus \u2014 Backup Timestamps').first().json?.data?.result || [];\nconst b2Results = $('Prometheus \u2014 B2 Sync Status').first().json?.data?.result || [];\n\nconst now = Date.now() / 1000;\nconst threshold = 25 * 3600; // 25 hours\n\n// Parse Velero last backup\nlet veleroOk = false;\nlet veleroMsg = 'No backup data';\ntry {\n  const veleroData = JSON.parse(veleroRaw);\n  const items = veleroData.items || [];\n  const completed = items.filter(i => i.status?.phase === 'Completed');\n  if (completed.length > 0) {\n    const last = completed.sort((a, b) => new Date(b.status.completionTimestamp) - new Date(a.status.completionTimestamp))[0];\n    const lastTs = new Date(last.status.completionTimestamp).getTime() / 1000;\n    veleroOk = (now - lastTs) < threshold;\n    veleroMsg = `Last: ${last.status.completionTimestamp.slice(0,16)} (${last.metadata.name})`;\n  }\n} catch(e) { veleroMsg = 'Parse error: ' + e.message; }\n\n// Check Prometheus backup metrics\nconst staleJobs = promTs.filter(r => {\n  const ts = parseFloat(r.value[1]);\n  return ts > 0 && (now - ts) > threshold;\n});\nconst neverJobs = promTs.filter(r => parseFloat(r.value[1]) === 0);\n\n// Check B2 sync\nconst b2Failed = b2Results.filter(r => parseFloat(r.value[1]) === 0);\n\nconst failures = [];\nif (!veleroOk) failures.push(`Velero: ${veleroMsg}`);\nif (staleJobs.length > 0) failures.push(`Stale backups: ${staleJobs.map(r => r.metric.job_name || r.metric.job).join(', ')}`);\nif (neverJobs.length > 0) failures.push(`Never succeeded: ${neverJobs.map(r => r.metric.job_name || r.metric.job).join(', ')}`);\nif (b2Failed.length > 0) failures.push('B2 offsite sync failed');\n\nreturn [{ json: { ok: failures.length === 0, failures, veleroMsg, ts: new Date().toISOString() } }];"
      },
      "id": "evaluate",
      "name": "Evaluate Results",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        700,
        400
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true
          },
          "conditions": [
            {
              "leftValue": "={{ $json.ok }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ]
        }
      },
      "id": "check-ok",
      "name": "All OK?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        920,
        400
      ]
    },
    {
      "parameters": {
        "url": "={{ $vars.DISCORD_CRITICAL_WEBHOOK }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "body",
              "value": "={{ JSON.stringify({ embeds: [{ title: '\ud83d\udd34 Backup Verification Failed', color: 15158332, fields: [ { name: 'Failures', value: $('Evaluate Results').first().json.failures.join('\\n'), inline: false }, { name: 'Velero', value: $('Evaluate Results').first().json.veleroMsg, inline: false } ], footer: { text: 'n8n backup check \u2014 03:30 SAST' }, timestamp: $('Evaluate Results').first().json.ts }] }) }}"
            }
          ]
        }
      },
      "id": "discord-alert",
      "name": "Discord \u2014 Backup Failure Alert",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1140,
        520
      ]
    }
  ],
  "connections": {
    "Cron \u2014 03:30 SAST daily": {
      "main": [
        [
          {
            "node": "SSH \u2014 Velero Backup Status",
            "type": "main",
            "index": 0
          },
          {
            "node": "Prometheus \u2014 Backup Timestamps",
            "type": "main",
            "index": 0
          },
          {
            "node": "Prometheus \u2014 B2 Sync Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SSH \u2014 Velero Backup Status": {
      "main": [
        [
          {
            "node": "Evaluate Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prometheus \u2014 Backup Timestamps": {
      "main": [
        [
          {
            "node": "Evaluate Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prometheus \u2014 B2 Sync Status": {
      "main": [
        [
          {
            "node": "Evaluate Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Evaluate Results": {
      "main": [
        [
          {
            "node": "All OK?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "All OK?": {
      "main": [
        [],
        [
          {
            "node": "Discord \u2014 Backup Failure Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "saveManualExecutions": true,
    "timezone": "Africa/Johannesburg"
  },
  "tags": [
    "homelab",
    "tier2",
    "backup"
  ],
  "notes": "W09: Daily 03:30 SAST backup verification. Checks Velero last completed backup (must be within 25h), Prometheus backup_last_success_timestamp metrics, and B2 offsite sync status. Silent on success \u2014 only posts to #homelab-critical on failure. Runs 30min after Velero (03:00) to allow completion."
}