{
  "nodes": [
    {
      "id": "b049d0bd-c963-4af8-93b4-b264249d06ff",
      "name": "Sticky Note - Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -304,
        -48
      ],
      "parameters": {
        "color": 4,
        "width": 500,
        "height": 560,
        "content": "## \ud83d\udcca n8n Resiliency Dashboard\n\nThis workflow generates a **real-time HTML dashboard** that monitors all your `[PROD]`-tagged workflows.\n\n### What it does\n- Lists all workflows tagged `[PROD]`\n- Fetches the last 50 success and 50 error executions per workflow\n- Calculates success rate, error count and last error timestamp\n- Renders an auto-refreshing HTML dashboard (every 30 seconds)\n- Serves it via Webhook \u2014 just open the URL in your browser\n\n### Setup\n1. Enable the n8n API: **Settings \u2192 n8n API**\n2. Create an **n8n API credential** and connect it to the two HTTP Request nodes\n3. Set your instance URL in the **Config** node\n4. Tag the workflows you want to monitor with `[PROD]`\n5. Activate this workflow and open the Webhook URL\n\n### Webhook URL\n`https://YOUR-N8N-URL/webhook/resiliency-dashboard`"
      },
      "typeVersion": 1
    },
    {
      "id": "4e5b8ef5-396b-464f-85ae-67f1461bfa82",
      "name": "Sticky Note - Config",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        288,
        -80
      ],
      "parameters": {
        "color": 3,
        "width": 260,
        "height": 196,
        "content": "### \u2699\ufe0f Step 1 \u2014 Config\nSet your n8n base URL here.\n\n**Example:**\n`https://your-instance.n8n.cloud`\n\nThis URL is reused by all HTTP Request nodes."
      },
      "typeVersion": 1
    },
    {
      "id": "acfacd6c-7846-48cc-a9a3-1e7201e0f7ef",
      "name": "Sticky Note - Fetch & Filter",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        544,
        -80
      ],
      "parameters": {
        "color": 3,
        "width": 370,
        "height": 196,
        "content": "### \ud83d\udd0d Step 2 \u2014 Fetch & Filter\nFetches all workflows via the n8n API, then filters only those tagged `[PROD]`.\n\nTo monitor a workflow, add the tag `[PROD]` to it in n8n Settings."
      },
      "typeVersion": 1
    },
    {
      "id": "eaf0fc85-3cc8-4fbb-b5e5-766bac347fca",
      "name": "Sticky Note - Executions",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        912,
        -80
      ],
      "parameters": {
        "color": 3,
        "width": 316,
        "height": 196,
        "content": "### \ud83d\udce5 Step 3 \u2014 Fetch Executions\nFor each `[PROD]` workflow, fetches the last **50 successes** and **50 errors** in parallel.\n\nBoth run simultaneously via branched connections."
      },
      "typeVersion": 1
    },
    {
      "id": "bda8db2d-3b5a-4a03-9eb6-347d78d69118",
      "name": "Sticky Note - Compare & Render",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1232,
        -80
      ],
      "parameters": {
        "color": 3,
        "width": 628,
        "height": 196,
        "content": "### \ud83d\udcca Step 4 \u2014 Compare & Render\nMerges success + error executions, computes per-workflow stats (success rate, error count, last error timestamp), builds a 7-day error chart and renders the full HTML dashboard, served via the Webhook response node."
      },
      "typeVersion": 1
    },
    {
      "id": "dd3be40c-a892-4397-9fcd-ec9f1e42cb40",
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "position": [
        1376,
        320
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineByPosition"
      },
      "typeVersion": 3.2
    },
    {
      "id": "674276f1-5a50-44ca-8806-038ef9ffefc6",
      "name": "Respond to Webhook",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        2048,
        320
      ],
      "parameters": {
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "text/html; charset=utf-8"
              }
            ]
          }
        },
        "respondWith": "text",
        "responseBody": "={{ $json.html }}"
      },
      "typeVersion": 1.4
    },
    {
      "id": "ee08f9c3-ce24-4f4c-a4ce-d50a616674a8",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        256,
        320
      ],
      "parameters": {
        "path": "resiliency-dashboard",
        "options": {},
        "responseMode": "responseNode"
      },
      "typeVersion": 2.1
    },
    {
      "id": "10ac5efd-4434-4f81-8448-9393ac45a36e",
      "name": "GET success",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1152,
        224
      ],
      "parameters": {
        "url": "={{ $('Config').item.json.base_url }}/api/v1/executions?status=success&limit=50&workflowId={{ $json.id }}",
        "options": {},
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "n8nApi"
      },
      "credentials": {
        "n8nApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "4e6d2c87-39d5-4105-aa92-7b5d57529b65",
      "name": "GET error",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1152,
        416
      ],
      "parameters": {
        "url": "={{ $('Config').item.json.base_url }}/api/v1/executions?status=error&limit=50&workflowId={{ $json.id }}",
        "options": {},
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "n8nApi"
      },
      "credentials": {
        "n8nApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "5bb16dd7-04ba-41df-bfc2-32073319d000",
      "name": "Compare",
      "type": "n8n-nodes-base.code",
      "disabled": true,
      "position": [
        1600,
        320
      ],
      "parameters": {
        "jsCode": "const mergeItems = $input.all();\n\nconst erros = [];\nconst sucessos = [];\n\nmergeItems.forEach(item => {\n  const data = item.json.data;\n  if (!data) return;\n  data.forEach(ex => {\n    if (ex.status === 'error') erros.push(ex);\n    else if (ex.status === 'success') sucessos.push(ex);\n  });\n});\n\nconst workflows = $('Filter: [PROD] Tag').all().map(i => i.json);\n\nconst workflowStats = workflows.map(wf => {\n  const wfErros = erros.filter(e => e.workflowId === wf.id);\n  const wfSucessos = sucessos.filter(e => e.workflowId === wf.id);\n  const total = wfErros.length + wfSucessos.length;\n  const taxaSucesso = total > 0 ? Math.round((wfSucessos.length / total) * 100) : 100;\n  const ultimoErro = wfErros.sort((a, b) => \n    new Date(b.startedAt) - new Date(a.startedAt)\n  )[0];\n\n  return {\n    id: wf.id,\n    name: wf.name,\n    totalErros: wfErros.length,\n    totalSucessos: wfSucessos.length,\n    taxaSucesso,\n    saudavel: wfErros.length === 0,\n    ultimoErro: ultimoErro ? ultimoErro.startedAt : null,\n  };\n});\n\nconst totalMonitorados = workflows.length;\nconst totalSaudaveis = workflowStats.filter(w => w.saudavel).length;\nconst totalComErro = workflowStats.filter(w => !w.saudavel).length;\n\nconst hoje = new Date();\nconst diasLabels = [];\nconst diasErros = [];\n\nfor (let i = 6; i >= 0; i--) {\n  const dia = new Date(hoje);\n  dia.setDate(dia.getDate() - i);\n  const label = dia.toLocaleDateString('en-US', { month: 'short', day: '2-digit' });\n  const count = erros.filter(e => {\n    const d = new Date(e.startedAt);\n    return d.toDateString() === dia.toDateString();\n  }).length;\n  diasLabels.push(label);\n  diasErros.push(count);\n}\n\nreturn [{\n  json: {\n    workflowStats,\n    totalMonitorados,\n    totalSaudaveis,\n    totalComErro,\n    diasLabels,\n    diasErros\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "68365082-a8dd-40b4-8f18-1af48964ac11",
      "name": "HTML",
      "type": "n8n-nodes-base.code",
      "position": [
        1824,
        320
      ],
      "parameters": {
        "jsCode": "const data = $input.first().json;\n\nconst {\n  workflowStats,\n  totalMonitorados,\n  totalSaudaveis,\n  totalComErro,\n  diasLabels,\n  diasErros\n} = data;\n\nconst generatedAt = new Date().toLocaleString('en-US', { timeZone: 'UTC' });\n\nconst workflowRows = workflowStats.map(wf => {\n  const statusPill = wf.saudavel\n    ? '<span class=\"pill success\">Healthy</span>'\n    : '<span class=\"pill error\">Error</span>';\n  const lastError = wf.ultimoErro\n    ? new Date(wf.ultimoErro).toLocaleString('en-US', { timeZone: 'UTC' })\n    : '\u2014';\n\n  return `\n    <tr>\n      <td>${wf.name}</td>\n      <td>${statusPill}</td>\n      <td class=\"mono\">${wf.taxaSucesso}%</td>\n      <td class=\"mono\">${wf.totalErros}</td>\n      <td class=\"mono\">${wf.totalSucessos}</td>\n      <td class=\"mono\">${lastError}</td>\n    </tr>`;\n}).join('');\n\nconst html = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta http-equiv=\"refresh\" content=\"30\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Resiliency Dashboard</title>\n<link href=\"https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Syne:wght@700;800&family=DM+Sans:wght@300;400;500&display=swap\" rel=\"stylesheet\">\n<script src=\"https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js\"></script>\n<style>\n  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n  :root {\n    --bg: #0a0d14; --surface: #111520; --surface-2: #181d2e;\n    --border: #1e2640; --accent: #4fffb0;\n    --red: #ff6b6b; --amber: #ffb347; --blue: #6b8cff;\n    --text: #e8eaf0; --text-muted: #5a6380; --text-dim: #8b94b2;\n    --font-display: 'Syne', sans-serif; --font-body: 'DM Sans', sans-serif;\n    --font-mono: 'DM Mono', monospace; --radius: 12px; --radius-sm: 7px;\n  }\n  body { background: var(--bg); color: var(--text); font-family: var(--font-body); min-height: 100vh; }\n  body::before {\n    content: ''; position: fixed; inset: 0;\n    background-image: linear-gradient(rgba(79,255,176,0.025) 1px, transparent 1px), linear-gradient(90deg, rgba(79,255,176,0.025) 1px, transparent 1px);\n    background-size: 40px 40px; pointer-events: none; z-index: 0;\n  }\n  .shell { position: relative; z-index: 1; max-width: 1280px; margin: 0 auto; padding: 40px 24px 80px; }\n  .header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 48px; padding-bottom: 24px; border-bottom: 1px solid var(--border); }\n  .badge { display: inline-flex; align-items: center; gap: 6px; font-family: var(--font-mono); font-size: 0.72rem; color: var(--accent); letter-spacing: 0.12em; text-transform: uppercase; margin-bottom: 4px; }\n  .badge::before { content: ''; width: 7px; height: 7px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 10px var(--accent); animation: pulse 2s ease-in-out infinite; }\n  @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }\n  h1 { font-family: var(--font-display); font-size: clamp(1.6rem, 3vw, 2.4rem); font-weight: 800; letter-spacing: -0.03em; line-height: 1.1; }\n  .header-right { font-family: var(--font-mono); font-size: 0.7rem; color: var(--text-muted); text-align: right; line-height: 1.9; }\n  .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-bottom: 32px; }\n  .stat-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px 22px; }\n  .stat-label { font-family: var(--font-mono); font-size: 0.65rem; color: var(--text-muted); letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 10px; }\n  .stat-value { font-family: var(--font-display); font-size: 2.2rem; font-weight: 800; line-height: 1; letter-spacing: -0.04em; }\n  .stat-value.green { color: #4fffb0; }\n  .stat-value.red { color: #ff6b6b; }\n  .stat-value.blue { color: #6b8cff; }\n  .stat-sub { font-size: 0.72rem; color: var(--text-muted); margin-top: 6px; font-weight: 300; }\n  .section { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 28px; margin-bottom: 32px; }\n  .section-title { font-family: var(--font-display); font-size: 0.9rem; font-weight: 700; letter-spacing: 0.04em; color: var(--text-dim); text-transform: uppercase; margin-bottom: 20px; display: flex; align-items: center; gap: 10px; }\n  .section-title::before { content: ''; display: block; width: 18px; height: 2px; background: var(--accent); border-radius: 2px; }\n  .chart-wrap { height: 220px; position: relative; }\n  .logs-scroll { overflow-x: auto; }\n  table { width: 100%; border-collapse: collapse; font-size: 0.78rem; }\n  thead th { font-family: var(--font-mono); font-size: 0.62rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-muted); text-align: left; padding: 10px 16px; border-bottom: 1px solid var(--border); white-space: nowrap; background: var(--surface-2); }\n  tbody tr { border-bottom: 1px solid rgba(30,38,64,0.6); transition: background 0.12s; }\n  tbody tr:hover { background: rgba(79,255,176,0.03); }\n  td { padding: 11px 16px; vertical-align: middle; }\n  td.mono { font-family: var(--font-mono); color: var(--text-dim); }\n  .pill { display: inline-flex; align-items: center; gap: 5px; padding: 3px 9px; border-radius: 99px; font-family: var(--font-mono); font-size: 0.65rem; font-weight: 500; letter-spacing: 0.06em; text-transform: uppercase; }\n  .pill.success { background: rgba(79,255,176,0.12); color: #4fffb0; }\n  .pill.error { background: rgba(255,107,107,0.12); color: #ff6b6b; }\n  .pill::before { content: ''; width: 5px; height: 5px; border-radius: 50%; background: currentColor; }\n  .refresh-btn { font-family: var(--font-mono); font-size: 0.7rem; background: transparent; border: 1px solid var(--border); color: var(--text-muted); padding: 7px 14px; border-radius: var(--radius-sm); cursor: pointer; transition: all 0.15s; }\n  .refresh-btn:hover { border-color: var(--accent); color: var(--accent); }\n</style>\n</head>\n<body>\n<div class=\"shell\">\n  <header class=\"header\">\n    <div>\n      <span class=\"badge\">Live</span>\n      <h1>Resiliency Dashboard</h1>\n      <p style=\"color:var(--text-muted);font-size:0.8rem;font-weight:300;margin-top:6px\">Real-time monitoring of [PROD] workflows</p>\n    </div>\n    <div class=\"header-right\">\n      <div>Generated at</div>\n      <div style=\"color:var(--text-dim)\">${generatedAt}</div>\n      <div style=\"margin-top:8px\"><button class=\"refresh-btn\" onclick=\"location.reload()\">\u21bb Refresh</button></div>\n    </div>\n  </header>\n\n  <div class=\"stats-grid\">\n    <div class=\"stat-card\">\n      <div class=\"stat-label\">Monitored</div>\n      <div class=\"stat-value blue\">${totalMonitorados}</div>\n      <div class=\"stat-sub\">[PROD] Workflows</div>\n    </div>\n    <div class=\"stat-card\">\n      <div class=\"stat-label\">Healthy</div>\n      <div class=\"stat-value green\">${totalSaudaveis}</div>\n      <div class=\"stat-sub\">No recent errors</div>\n    </div>\n    <div class=\"stat-card\">\n      <div class=\"stat-label\">With errors</div>\n      <div class=\"stat-value red\">${totalComErro}</div>\n      <div class=\"stat-sub\">Need attention</div>\n    </div>\n  </div>\n\n  <div class=\"section\">\n    <div class=\"section-title\">Errors per day \u2014 last 7 days</div>\n    <div class=\"chart-wrap\"><canvas id=\"chart\"></canvas></div>\n  </div>\n\n  <div class=\"section\">\n    <div class=\"section-title\">Workflow status</div>\n    <div class=\"logs-scroll\">\n      <table>\n        <thead>\n          <tr>\n            <th>Workflow</th>\n            <th>Status</th>\n            <th>Success rate</th>\n            <th>Errors</th>\n            <th>Successes</th>\n            <th>Last error</th>\n          </tr>\n        </thead>\n        <tbody>${workflowRows}</tbody>\n      </table>\n    </div>\n  </div>\n</div>\n\n<script>\nnew Chart(document.getElementById('chart').getContext('2d'), {\n  type: 'bar',\n  data: {\n    labels: ${JSON.stringify(diasLabels)},\n    datasets: [{\n      label: 'Errors',\n      data: ${JSON.stringify(diasErros)},\n      backgroundColor: 'rgba(255,107,107,0.25)',\n      borderColor: 'rgba(255,107,107,0.8)',\n      borderWidth: 1,\n      borderRadius: 4\n    }]\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    plugins: { legend: { labels: { color: '#5a6380', font: { family: 'DM Mono', size: 11 } } } },\n    scales: {\n      x: { grid: { color: 'rgba(30,38,64,0.6)' }, ticks: { color: '#5a6380', font: { family: 'DM Mono', size: 10 } } },\n      y: { grid: { color: 'rgba(30,38,64,0.6)' }, ticks: { color: '#5a6380', font: { family: 'DM Mono', size: 10 } } }\n    }\n  }\n});\n</script>\n</body>\n</html>`;\n\nreturn [{ json: { html } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "84da998d-421b-4f28-842d-1a93eb6767c3",
      "name": "Config",
      "type": "n8n-nodes-base.set",
      "position": [
        480,
        320
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "f505883e-8474-4d45-b9d0-474cb4f8ceac",
              "name": "base_url",
              "type": "string",
              "value": "https://YOUR-N8N-URL-HERE"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "d60eac82-4de8-4621-8af9-ae8c665a184b",
      "name": "API: List Workflows",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        704,
        320
      ],
      "parameters": {
        "url": "={{ $('Config').item.json.base_url }}/api/v1/workflows ",
        "options": {},
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "n8nApi"
      },
      "credentials": {
        "n8nApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "0f66b66b-a520-4378-b75d-5737632f35a8",
      "name": "Filter: [PROD] Tag",
      "type": "n8n-nodes-base.code",
      "position": [
        928,
        320
      ],
      "parameters": {
        "jsCode": "const response = $input.first().json;\nconst workflows = response.data;\n\nconst prodWorkflows = workflows.filter(wf => \n  wf.tags && wf.tags.some(tag => tag.name === \"[PROD]\")\n);\n\nreturn prodWorkflows.map(wf => ({\n  json: {\n    id: wf.id,\n    name: wf.name,\n    updatedAt: wf.updatedAt,\n    versionId: wf.versionId,\n    workflow: wf\n  }\n}));"
      },
      "typeVersion": 2
    }
  ],
  "connections": {
    "HTML": {
      "main": [
        [
          {
            "node": "Respond to Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge": {
      "main": [
        [
          {
            "node": "Compare",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config": {
      "main": [
        [
          {
            "node": "API: List Workflows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compare": {
      "main": [
        [
          {
            "node": "HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook": {
      "main": [
        [
          {
            "node": "Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GET error": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "GET success": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter: [PROD] Tag": {
      "main": [
        [
          {
            "node": "GET error",
            "type": "main",
            "index": 0
          },
          {
            "node": "GET success",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "API: List Workflows": {
      "main": [
        [
          {
            "node": "Filter: [PROD] Tag",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}