server-desktop-01/scripts/n8n-workflow-reporter.json

210 lines
8.8 KiB
JSON

{
"name": "Backup Monitor — Reporter",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "weeks",
"weeksInterval": 1,
"triggerAtDay": [
5
],
"triggerAtHour": 8,
"triggerAtMinute": 0
}
]
}
},
"id": "f6fb320b-42e8-4894-ac52-d014dbab8ab1",
"name": "Every Friday 8 AM",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
240,
300
]
},
{
"parameters": {
"mode": "manual",
"includeOtherFields": false,
"assignments": {
"assignments": [
{
"id": "b46a89e2-f93f-4219-84cd-cf14a28d26c4",
"name": "sources",
"value": "={{ [{\"machine\":\"desktop\",\"destination\":\"synology\"},{\"machine\":\"desktop\",\"destination\":\"backblaze-b2\"},{\"machine\":\"vps-ovh-prod-01\",\"destination\":\"local\"}] }}",
"type": "arrayValue"
}
]
},
"options": {}
},
"id": "251bc404-f466-4922-b2d8-1e7d8642e151",
"name": "Set sources",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
460,
300
]
},
{
"parameters": {
"operation": "getAll",
"dataTableId": {
"__rl": true,
"mode": "list",
"value": "cWeJhlybF9eV43DB",
"cachedResultName": "backup_runs"
},
"options": {}
},
"id": "7eec8f10-b3ed-4059-bd33-3bafccb56d03",
"name": "Get backup_runs (last 7d)",
"type": "n8n-nodes-base.dataTable",
"typeVersion": 1,
"position": [
680,
300
]
},
{
"parameters": {
"mode": "runOnceForAllItems",
"language": "javaScript",
"jsCode": "// Runs once for all items. References upstream nodes by exact name.\nconst sources = $('Set sources').first().json.sources;\nconst runs = $('Get backup_runs (last 7d)').all().map(item => item.json);\nconst weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);\n\nconst results = sources.map(source => {\n const sourceRuns = runs.filter(r =>\n r.machine === source.machine &&\n r.destination === source.destination &&\n new Date(r.timestamp) >= weekAgo\n );\n const hasSuccess = sourceRuns.some(r => r.status === 'success');\n const hasAnyRun = sourceRuns.length > 0;\n const status = hasSuccess ? 'green' : hasAnyRun ? 'yellow' : 'red';\n const lastSuccess = sourceRuns\n .filter(r => r.status === 'success')\n .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))[0] || null;\n return { ...source, status, runs: sourceRuns, lastSuccess };\n});\n\nconst overallStatus = results.some(r => r.status === 'red') ? 'red'\n : results.some(r => r.status === 'yellow') ? 'yellow' : 'green';\n\nreturn [{ json: { results, overallStatus } }];\n"
},
"id": "2739ccac-48a4-4736-b7e6-6da384794ec9",
"name": "Evaluate Sources",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
900,
300
]
},
{
"parameters": {
"mode": "runOnceForAllItems",
"language": "javaScript",
"jsCode": "// Builds subject + body from Evaluate Sources output.\n// Uses an array of lines joined at the end to avoid \\n escape issues in JSON storage.\nconst { results, overallStatus } = $input.first().json;\nconst icon = { green: '✓', yellow: '⚠', red: '✗' };\nconst label = { green: 'Backups healthy', yellow: 'Backup warning', red: 'Backup failure' };\nconst weekOf = new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric' });\nconst subject = icon[overallStatus] + ' ' + label[overallStatus] + ' — week of ' + weekOf;\n\nconst fmt = ts => ts\n ? new Date(ts).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })\n : 'unknown';\n\nconst L = [];\n\nif (overallStatus === 'green') {\n L.push('All backup sources ran successfully this week.');\n L.push('');\n results.forEach(r =>\n L.push('✓ ' + r.machine + ' / ' + r.destination +\n ' (last success: ' + fmt(r.lastSuccess && r.lastSuccess.timestamp) + ')')\n );\n} else {\n const problems = results.filter(r => r.status !== 'green');\n const healthy = results.filter(r => r.status === 'green');\n\n L.push(problems.length + ' source(s) need attention:');\n L.push('');\n\n problems.forEach(r => {\n L.push(icon[r.status] + ' ' + r.machine + ' / ' + r.destination);\n\n if (r.status === 'yellow') {\n L.push(' Ran this week but never succeeded.');\n r.runs.slice(0, 3).forEach(run => {\n var line = ' ' + fmt(run.timestamp) + ' [' + run.status + ']';\n if (run.log_path) line += ' log: ' + run.log_path;\n L.push(line);\n if (run.notes) L.push(' notes: ' + (run.notes || '').slice(0, 300));\n });\n L.push('');\n L.push(' Next steps:');\n L.push(' 1. Check the log at the path shown above');\n L.push(' 2. For Synology failures: verify LAN reachability (ssh synology) — disks need ~30s to spin up');\n L.push(' 3. Re-run manually: bash ~/.local/bin/backup.sh');\n L.push(' 4. One-off skip (power outage, travel) is expected — yellow is a heads-up, not an alarm');\n L.push('');\n }\n\n if (r.status === 'red') {\n L.push(' No runs reported all week — silent failure.');\n if (r.lastSuccess) {\n L.push(' Last known good run: ' + fmt(r.lastSuccess.timestamp));\n if (r.lastSuccess.log_path) L.push(' Last log: ' + r.lastSuccess.log_path);\n } else {\n L.push(' No history found in the datatable.');\n }\n L.push('');\n L.push(' Next steps:');\n L.push(' 1. Check systemd timer: systemctl --user status restic-backup.timer');\n L.push(' 2. Verify backup.sh calls notify_backup.rb at the end');\n L.push(' 3. Check n8n Workflow 1 execution log for webhook errors');\n L.push(' 4. Run manually: bash ~/.local/bin/backup.sh');\n L.push('');\n }\n });\n\n if (healthy.length > 0) {\n L.push('Healthy sources:');\n healthy.forEach(r => L.push('✓ ' + r.machine + ' / ' + r.destination));\n }\n}\n\nreturn [{ json: { subject, body: L.join('\\n') } }];\n"
},
"id": "6d6e9968-2a9a-4e52-8c94-72d6ec7552b8",
"name": "Build Email",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1120,
300
]
},
{
"parameters": {
"method": "POST",
"url": "https://app.mailpace.com/api/v1/send",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/json"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ { from: \"backups@swansoncloud.com\", to: \"jaredmswanson@gmail.com\", subject: $json.subject, textbody: $json.body } }}",
"options": {}
},
"id": "a05f5f30-df7e-4df7-bef0-15f6f7f9ceca",
"name": "Send via MailPace",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1340,
300
],
"credentials": {
"httpHeaderAuth": {
"id": "tC1vdhAHGVwAZxcg",
"name": "mailpace-token"
}
}
}
],
"connections": {
"Every Friday 8 AM": {
"main": [
[
{
"node": "Set sources",
"type": "main",
"index": 0
}
]
]
},
"Set sources": {
"main": [
[
{
"node": "Get backup_runs (last 7d)",
"type": "main",
"index": 0
}
]
]
},
"Get backup_runs (last 7d)": {
"main": [
[
{
"node": "Evaluate Sources",
"type": "main",
"index": 0
}
]
]
},
"Evaluate Sources": {
"main": [
[
{
"node": "Build Email",
"type": "main",
"index": 0
}
]
]
},
"Build Email": {
"main": [
[
{
"node": "Send via MailPace",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"pinData": {},
"meta": {
"templateCredsSetupCompleted": true
},
"tags": []
}