{
  "id": "Twcq80VMRVqPoakz",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Webpage Down Detector - revised",
  "tags": [],
  "nodes": [
    {
      "id": "08dfb3ba-fa13-4aa7-bcf4-dc1cf3bbb34e",
      "name": "Sticky Note \u2014 Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -288,
        -192
      ],
      "parameters": {
        "width": 500,
        "height": 432,
        "content": "## Webpage Down Detector\n\nMonitors all your webpages daily and emails a detailed alert if any are down.\n\n**Features:**\n- Checks all URLs from Google Sheets\n- Retries failed URLs once to avoid false positives\n- Sends a rich HTML email with status codes & response times\n- Sends nothing if all pages are up (no noise)\n- Includes timestamp and summary stats\n\n**Setup:**\n1. Add your URLs to the Google Sheet\n2. Set your Google Sheets credentials\n3. Set your Gmail credentials\n4. Update recipient email in the Gmail node"
      },
      "typeVersion": 1
    },
    {
      "id": "61fdaaa8-1482-4a1f-9896-2edfae21f774",
      "name": "Sticky Note \u2014 Step 1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        240,
        -192
      ],
      "parameters": {
        "color": 7,
        "width": 352,
        "height": 424,
        "content": "## Step 1\nRuns every day at 8am and fetches all your URLs from Google Sheets"
      },
      "typeVersion": 1
    },
    {
      "id": "c820ef85-635a-465b-936d-30f4bc63adad",
      "name": "Sticky Note \u2014 Step 2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        624,
        -192
      ],
      "parameters": {
        "color": 7,
        "width": 320,
        "height": 424,
        "content": "## Step 2\nChecks each URL. If it fails, retries once after 5 seconds to avoid false positives"
      },
      "typeVersion": 1
    },
    {
      "id": "27fee5f8-a621-4f05-a735-e98eb81d84d5",
      "name": "Sticky Note \u2014 Step 3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        992,
        -192
      ],
      "parameters": {
        "color": 7,
        "width": 480,
        "height": 424,
        "content": "## Step 3\nFilters down pages, checks if any exist, then sends a rich HTML alert email with full details"
      },
      "typeVersion": 1
    },
    {
      "id": "ab8b8774-2674-4c34-93b7-56c5f232e269",
      "name": "Runs Every Day at 8am",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        288,
        -16
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * *"
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "04e9e21b-e003-4fc8-94c0-bdfbc6298afb",
      "name": "Fetch All Webpages",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        464,
        -16
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1bn40GRB-Snm7eT9buivMVY-Ba1cao7i-0wGIqHozrD4/edit#gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1bn40GRB-Snm7eT9buivMVY-Ba1cao7i-0wGIqHozrD4",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1bn40GRB-Snm7eT9buivMVY-Ba1cao7i-0wGIqHozrD4/edit?usp=drivesdk",
          "cachedResultName": "urls"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "3f42dab3-c215-42b8-aeca-d59af98b2079",
      "name": "Check All Webpages (with Retry)",
      "type": "n8n-nodes-base.code",
      "position": [
        752,
        -16
      ],
      "parameters": {
        "jsCode": "// Check each URL and retry once if it fails\n// Records status code and response time\n\nasync function fetchStatus(url) {\n  const start = Date.now();\n  try {\n    await this.helpers.request({\n      method: 'GET',\n      url: url,\n      timeout: 10000,\n      rejectUnauthorized: false,\n    });\n    return { status: 200, responseTime: Date.now() - start };\n  } catch (error) {\n    return { status: error.statusCode || 'ERROR', responseTime: Date.now() - start };\n  }\n}\n\nasync function sleep(ms) {\n  return new Promise(resolve => setTimeout(resolve, ms));\n}\n\nconst results = await Promise.all(\n  items.map(async (item) => {\n    const url = item.json.URL;\n    if (!url) return null;\n\n    // First attempt\n    let result = await fetchStatus.call(this, url);\n\n    // Retry once if failed\n    const isDown = result.status === 'ERROR' || (typeof result.status === 'number' && (result.status < 200 || result.status >= 400));\n    if (isDown) {\n      await sleep(5000); // wait 5 seconds before retry\n      result = await fetchStatus.call(this, url);\n    }\n\n    const stillDown = result.status === 'ERROR' || (typeof result.status === 'number' && (result.status < 200 || result.status >= 400));\n\n    return {\n      json: {\n        URL: url,\n        status: result.status,\n        responseTime: result.responseTime,\n        isDown: stillDown,\n        retried: isDown\n      }\n    };\n  })\n);\n\nreturn results.filter(r => r !== null);\n"
      },
      "typeVersion": 2
    },
    {
      "id": "6dfa15f9-bb03-4a46-b800-42a11143bf84",
      "name": "Filter Down Webpages",
      "type": "n8n-nodes-base.code",
      "position": [
        1088,
        -16
      ],
      "parameters": {
        "jsCode": "// Separate down pages from up pages\n// Pass both counts forward for the summary email\n\nconst allPages = items.map(i => i.json);\nconst downPages = allPages.filter(p => p.isDown);\nconst upCount = allPages.length - downPages.length;\n\nif (downPages.length === 0) {\n  // Return special flag so next node can stop the workflow\n  return [{ json: { hasDownPages: false, totalMonitored: allPages.length, downCount: 0, upCount } }];\n}\n\nreturn [{\n  json: {\n    hasDownPages: true,\n    totalMonitored: allPages.length,\n    downCount: downPages.length,\n    upCount,\n    downPages,\n    checkedAt: new Date().toLocaleString('en-AU', { timeZone: 'Australia/Sydney' })\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "1dece255-57b7-4078-a606-ee38db428c14",
      "name": "Any Pages Down?",
      "type": "n8n-nodes-base.if",
      "position": [
        1312,
        -16
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "check-down",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.hasDownPages }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "26de92a6-f8fc-49d5-a2bf-c392b28d1e29",
      "name": "Compose HTML Email",
      "type": "n8n-nodes-base.code",
      "position": [
        1584,
        -80
      ],
      "parameters": {
        "jsCode": "const data = $input.first().json;\nconst { downPages, totalMonitored, downCount, upCount, checkedAt } = data;\n\n// Build table rows for each down page\nconst rows = downPages.map(page => {\n  const statusLabel = page.status === 'ERROR' ? '\u26a0\ufe0f ERROR' : `\u274c ${page.status}`;\n  const retryNote = page.retried ? '<span style=\"color:#888;font-size:11px\">(confirmed after retry)</span>' : '';\n  return `\n    <tr>\n      <td style=\"padding:10px;border-bottom:1px solid #f0f0f0;word-break:break-all\">\n        <a href=\"${page.URL}\" style=\"color:#e53e3e\">${page.URL}</a>\n      </td>\n      <td style=\"padding:10px;border-bottom:1px solid #f0f0f0;text-align:center;font-weight:bold;color:#e53e3e\">${statusLabel}</td>\n      <td style=\"padding:10px;border-bottom:1px solid #f0f0f0;text-align:center;color:#666\">${page.responseTime}ms ${retryNote}</td>\n    </tr>`;\n}).join('');\n\nconst html = `\n<!DOCTYPE html>\n<html>\n<body style=\"font-family:Arial,sans-serif;background:#f9f9f9;margin:0;padding:20px\">\n  <div style=\"max-width:640px;margin:0 auto;background:#fff;border-radius:8px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.08)\">\n    \n    <!-- Header -->\n    <div style=\"background:#e53e3e;padding:24px 32px\">\n      <h1 style=\"color:#fff;margin:0;font-size:22px\">\ud83d\udea8 Webpage Down Alert</h1>\n      <p style=\"color:#fed7d7;margin:8px 0 0\">Checked at ${checkedAt} (AEST)</p>\n    </div>\n\n    <!-- Summary -->\n    <div style=\"padding:24px 32px;background:#fff5f5;border-bottom:1px solid #fed7d7\">\n      <table style=\"width:100%;border-collapse:collapse\">\n        <tr>\n          <td style=\"text-align:center;padding:12px\">\n            <div style=\"font-size:32px;font-weight:bold;color:#e53e3e\">${downCount}</div>\n            <div style=\"color:#666;font-size:13px\">Pages Down</div>\n          </td>\n          <td style=\"text-align:center;padding:12px\">\n            <div style=\"font-size:32px;font-weight:bold;color:#38a169\">${upCount}</div>\n            <div style=\"color:#666;font-size:13px\">Pages Up</div>\n          </td>\n          <td style=\"text-align:center;padding:12px\">\n            <div style=\"font-size:32px;font-weight:bold;color:#4a5568\">${totalMonitored}</div>\n            <div style=\"color:#666;font-size:13px\">Total Monitored</div>\n          </td>\n        </tr>\n      </table>\n    </div>\n\n    <!-- Table -->\n    <div style=\"padding:24px 32px\">\n      <h2 style=\"font-size:16px;color:#2d3748;margin:0 0 16px\">Down Pages Details</h2>\n      <table style=\"width:100%;border-collapse:collapse;font-size:14px\">\n        <thead>\n          <tr style=\"background:#f7fafc\">\n            <th style=\"padding:10px;text-align:left;color:#4a5568;border-bottom:2px solid #e2e8f0\">URL</th>\n            <th style=\"padding:10px;text-align:center;color:#4a5568;border-bottom:2px solid #e2e8f0\">Status</th>\n            <th style=\"padding:10px;text-align:center;color:#4a5568;border-bottom:2px solid #e2e8f0\">Response Time</th>\n          </tr>\n        </thead>\n        <tbody>${rows}</tbody>\n      </table>\n    </div>\n\n    <!-- Footer -->\n    <div style=\"padding:16px 32px;background:#f7fafc;border-top:1px solid #e2e8f0\">\n      <p style=\"color:#a0aec0;font-size:12px;margin:0\">This alert was sent automatically by your n8n Webpage Monitor. All failed URLs were retried once before alerting.</p>\n    </div>\n  </div>\n</body>\n</html>`;\n\nreturn [{ json: { html, subject: `\ud83d\udea8 ${downCount} Webpage(s) Down \u2014 ${checkedAt}` } }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "591794ee-7b72-42c1-a13a-17b9dfcae146",
      "name": "Send Alert Email",
      "type": "n8n-nodes-base.gmail",
      "position": [
        1792,
        -80
      ],
      "parameters": {
        "sendTo": "user@example.com",
        "message": "={{ $json.html }}",
        "options": {},
        "subject": "={{ $json.subject }}"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "35ed5b33-ba6d-4477-afd8-97a325e03c09",
      "name": "Sticky Note \u2014 Step ",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1520,
        -192
      ],
      "parameters": {
        "color": 7,
        "width": 480,
        "height": 424,
        "content": "## Step 4\nCompose and send email"
      },
      "typeVersion": 1
    },
    {
      "id": "bb6d3dce-b34b-4328-88f2-947e5e3e13c0",
      "name": "All Pages Up - Stop",
      "type": "n8n-nodes-base.noOp",
      "position": [
        1584,
        80
      ],
      "parameters": {},
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "bae47254-fd29-44a8-acd4-96176d8d44ed",
  "connections": {
    "Any Pages Down?": {
      "main": [
        [
          {
            "node": "Compose HTML Email",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "All Pages Up - Stop",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compose HTML Email": {
      "main": [
        [
          {
            "node": "Send Alert Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch All Webpages": {
      "main": [
        [
          {
            "node": "Check All Webpages (with Retry)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Down Webpages": {
      "main": [
        [
          {
            "node": "Any Pages Down?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Runs Every Day at 8am": {
      "main": [
        [
          {
            "node": "Fetch All Webpages",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check All Webpages (with Retry)": {
      "main": [
        [
          {
            "node": "Filter Down Webpages",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}