{
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "c430dbe3-a6b8-4e18-8cf8-a5e42a495f46",
      "name": "When clicking \u2018Execute workflow\u2019",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        5856,
        1920
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "a63f4cbf-2477-4662-b651-af1c14332d32",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4528,
        1408
      ],
      "parameters": {
        "width": 1200,
        "height": 1184,
        "content": "# Hosting Static HTML KPI Dashboard with CustomJS\n\n[KPI Google Sheet](https://docs.google.com/spreadsheets/d/10mj6hngkPTg1_A6bVNn8QfKujap6j-Lrt_LwAkY2d7Q/edit?usp=sharing)  \n\nThis workflow automatically generates a weekly KPI dashboard from Google Sheets data and hosts it as a live static HTML page using [CustomJS](https://www.customjs.space).\n\n---\n\n## Setup\n- Requires a **self-hosted n8n instance** and a **CustomJS API key**.\n- Google Sheets should be **publicly accessible** or shared with a service account for n8n access.\n- Optional: Additional JSON/CSV sources can be integrated.\n\n---\n\n## How it works\n1 **Manual Trigger**  \n- Run the workflow on demand or schedule weekly.\n\n\n2 **Load Data from Google Sheet**  \n- Pull KPI metrics such as Visitors, Leads, Demo Booked, Proposal Sent, Won.  \n- Aggregate weekly or per channel as needed.\n\n\n3 **Prepare Structured JSON**  \n- Transform sheet rows into a clean JSON structure ready for the dashboard.  \n- Example structure: \n```json\n[\n  {\"Date\":\"2026-02-01\",\"Channel\":\"Google Ads\",\"Visitors\":1200,\"Leads\":95,\"Demo Booked\":40,\"Proposal Sent\":22,\"Won\":9},\n  {\"Date\":\"2026-02-01\",\"Channel\":\"LinkedIn\",\"Visitors\":800,\"Leads\":70,\"Demo Booked\":28,\"Proposal Sent\":16,\"Won\":7}\n]\n```\n\n4 **Generate HTML Dashboard**  \n- Feed JSON into a CustomJS HTML template node.\n- Automatically builds KPI cards, tables, and charts using Chart.js.\n\n\n5 **Host Static HTML**\n- CustomJS deploys the HTML page instantly.\n- Optional: Connect a custom domain in one click.\n- Each weekly update overwrites the previous dashboard.\n\n\n6 **Optional Enhancements**\n- Generate QR codes for the live dashboard link.\n- Include multiple charts, multiple sheets, or historical trends."
      },
      "typeVersion": 1
    },
    {
      "id": "63aad3dc-26fd-4427-ab40-762761c6b1fe",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5776,
        1824
      ],
      "parameters": {
        "color": 7,
        "width": 960,
        "height": 416,
        "content": "## Collect sheet data and convert to json"
      },
      "typeVersion": 1
    },
    {
      "id": "dd1a3ad4-505f-4222-a05c-e66bd9332c99",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        6848,
        1824
      ],
      "parameters": {
        "color": 7,
        "width": 544,
        "height": 416,
        "content": "## Build and publish HTML"
      },
      "typeVersion": 1
    },
    {
      "id": "63ab98d5-413e-4c9f-9334-742c5a87c3b4",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        7504,
        1840
      ],
      "parameters": {
        "color": 7,
        "width": 432,
        "height": 400,
        "content": "## Generate QR code"
      },
      "typeVersion": 1
    },
    {
      "id": "3ceea1d1-37dd-48f3-a571-4a9bbbe02479",
      "name": "HTML",
      "type": "n8n-nodes-base.html",
      "position": [
        6960,
        1968
      ],
      "parameters": {
        "html": "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"UTF-8\">\n<title>KPI Dashboard</title>\n<script src=\"https://cdn.jsdelivr.net/npm/chart.js\"></script>\n\n<style>\nbody{\n  font-family: system-ui, -apple-system, sans-serif;\n  background:#f6f7fb;\n  margin:0;\n  padding:24px;\n  color:#111;\n}\n.container{\n  max-width:1100px;\n  margin:auto;\n}\nh1{margin-bottom:4px;}\n.subtitle{color:#666;margin-bottom:30px;}\n\n.cards{\n  display:grid;\n  grid-template-columns:repeat(auto-fit,minmax(180px,1fr));\n  gap:16px;\n  margin-bottom:30px;\n}\n.card{\n  background:white;\n  padding:18px;\n  border-radius:12px;\n  box-shadow:0 2px 6px rgba(0,0,0,0.06);\n}\n.card .label{font-size:13px;color:#666}\n.card .value{font-size:24px;font-weight:700;margin-top:6px}\n\n.charts{\n  display:grid;\n  grid-template-columns:repeat(auto-fit, minmax(300px,1fr));\n  gap:24px;\n  margin-bottom:24px;\n}\n@media(max-width:900px){\n  .charts{\n    grid-template-columns:1fr;\n  }\n}\ncanvas{\n  max-width:100%;\n  width:100%;\n  height:280px;\n  box-sizing:border-box;\n}\n\n.section{\n  background:white;\n  padding:18px;\n  border-radius:12px;\n  margin-bottom:24px;\n  box-shadow:0 2px 6px rgba(0,0,0,0.06);\n}\n\ntable{\n  width:100%;\n  border-collapse:collapse;\n}\nth,td{\n  padding:10px;\n  border-bottom:1px solid #eee;\n  text-align:left;\n  font-size:14px;\n}\nth{color:#666;font-weight:600;}\n\n.footer{\n  text-align:center;\n  margin-top:30px;\n  color:#999;\n  font-size:12px;\n}\n</style>\n</head>\n\n<body>\n<div class=\"container\">\n\n<h1 id=\"title\"></h1>\n<div class=\"subtitle\" id=\"period\"></div>\n\n<div class=\"cards\" id=\"kpiCards\"></div>\n\n<div class=\"charts\">\n\n  <div class=\"section\">\n    <h3>Visitors by Channel</h3>\n    <canvas id=\"visitorsChart\"></canvas>\n  </div>\n\n  <div class=\"section\">\n    <h3>Funnel Conversion</h3>\n    <canvas id=\"funnelChart\"></canvas>\n  </div>\n\n</div>\n\n<div class=\"section\">\n  <h3>Channel Breakdown</h3>\n  <table id=\"table\"></table>\n</div>\n\n<div class=\"footer\">\nGenerated automatically \u2022 KPI Dashboard\n</div>\n\n</div>\n\n<script>\n/* ===== JSON Daten ===== */\nconst report = {\n  reportTitle: \"Weekly Sales Funnel Report\",\n  period: \"Week ending 2026-02-01\",\n  data: {{ JSON.stringify($json.data) }}\n};\n\n/* ===== Totals berechnen ===== */\nconst totals = report.data.reduce((a,c)=>({\n  visitors: a.visitors + (Number(c[\"Visitors\"]) || 0),\n  leads: a.leads + (Number(c[\"Leads\"]) || 0),\n  demo: a.demo + (Number(c[\"Demo Booked\"]) || 0),\n  proposal: a.proposal + (Number(c[\"Proposal Sent\"]) || 0),\n  won: a.won + (Number(c[\"Won\"]) || 0)\n}), {visitors:0, leads:0, demo:0, proposal:0, won:0});\n\n/* ===== KPIs ===== */\nconst leadRate = totals.visitors\n  ? ((totals.leads / totals.visitors)*100).toFixed(1)\n  : \"0.0\";\n\nconst closeRate = totals.leads\n  ? ((totals.won / totals.leads)*100).toFixed(1)\n  : \"0.0\";\n\n/* ===== HEADER ===== */\ndocument.getElementById(\"title\").textContent = report.reportTitle;\ndocument.getElementById(\"period\").textContent = report.period;\n\n/* ===== KPI CARDS ===== */\nconst cards = [\n  [\"Visitors\", totals.visitors],\n  [\"Leads\", totals.leads],\n  [\"Deals Won\", totals.won],\n  [\"Lead Rate\", leadRate+\"%\"],\n  [\"Close Rate\", closeRate+\"%\"]\n];\ndocument.getElementById(\"kpiCards\").innerHTML = cards.map(c=>`\n<div class=\"card\">\n  <div class=\"label\">${c[0]}</div>\n  <div class=\"value\">${c[1]}</div>\n</div>`).join(\"\");\n\n/* ===== TABLE ===== */\ndocument.getElementById(\"table\").innerHTML = `\n<tr>\n  <th>Channel</th>\n  <th>Visitors</th>\n  <th>Leads</th>\n  <th>Demo</th>\n  <th>Proposal</th>\n  <th>Won</th>\n</tr>` + report.data.map(r=>`\n<tr>\n  <td>${r[\"Channel\"]}</td>\n  <td>${r[\"Visitors\"]}</td>\n  <td>${r[\"Leads\"]}</td>\n  <td>${r[\"Demo Booked\"]}</td>\n  <td>${r[\"Proposal Sent\"]}</td>\n  <td>${r[\"Won\"]}</td>\n</tr>`).join(\"\");\n\n/* ===== VISITORS CHART ===== */\nnew Chart(document.getElementById('visitorsChart'),{\n  type:'bar',\n  data:{\n    labels: report.data.map(r=>r[\"Channel\"]),\n    datasets:[{\n      label:'Visitors',\n      data: report.data.map(r=>Number(r[\"Visitors\"]) || 0),\n      backgroundColor:['#4f46e5','#10b981','#f59e0b']\n    }]\n  },\n  options:{\n    responsive:true,\n    plugins:{legend:{display:false}},\n    layout:{padding:8}\n  }\n});\n\n/* ===== FUNNEL CHART ===== */\nnew Chart(document.getElementById('funnelChart'),{\n  type:'line',\n  data:{\n    labels:[\"Visitors\",\"Leads\",\"Demo\",\"Proposal\",\"Won\"],\n    datasets:[{\n      label:'Total Funnel',\n      data:[\n        totals.visitors,\n        totals.leads,\n        totals.demo,\n        totals.proposal,\n        totals.won\n      ],\n      borderColor:'#4f46e5',\n      backgroundColor:'rgba(79,70,229,0.2)',\n      tension:0.4,\n      fill:true\n    }]\n  },\n  options:{\n    responsive:true,\n    plugins:{legend:{display:false}},\n    layout:{padding:8}\n  }\n});\n</script>\n</body>\n</html>"
      },
      "typeVersion": 1.2
    },
    {
      "id": "af3d762b-f5c4-4b39-8f0a-8bf275e53dc7",
      "name": "Get Data From Sheet",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        6096,
        1968
      ],
      "parameters": {
        "url": "https://docs.google.com/spreadsheets/d/10mj6hngkPTg1_A6bVNn8QfKujap6j-Lrt_LwAkY2d7Q/gviz/tq?tqx=out:csv&sheet=Sheet1",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "effc82e4-f01b-4984-8b4b-5353d203f4f7",
      "name": "Convert HTML to PDF",
      "type": "@custom-js/n8n-nodes-pdf-toolkit-v2.pdfToolkit",
      "position": [
        7632,
        1968
      ],
      "parameters": {
        "html": "=<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"UTF-8\">\n<title>KPI Dashbaord</title>\n<script src=\"https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js\"></script>\n<style>\nbody {\n  font-family: system-ui, -apple-system, sans-serif;\n  background: #f6f7fb;\n  margin: 0;\n  padding: 24px;\n  color: #111;\n}\n.container {\n  max-width: 400px;\n  margin: auto;\n  background: #fff;\n  padding: 24px;\n  border-radius: 12px;\n  box-shadow: 0 2px 8px rgba(0,0,0,0.1);\n}\nh1 {\n  margin-bottom: 24px;\n}\n#qrcode {\n  margin-top: 24px;\n}\n</style>\n</head>\n<body>\n\n<div class=\"container\">\n  <h1>KPI Dashboard</h1>\n  <div id=\"qrcode\"></div>\n</div>\n\n<script>\nconst url = \"{{ $json.htmlFileUrl }}\";\n\nnew QRCode(document.getElementById(\"qrcode\"), {\n  text: url,\n  width: 200,\n  height: 200,\n  colorDark: \"#4f46e5\",\n  colorLight: \"#ffffff\",\n  correctLevel: QRCode.CorrectLevel.H\n});\n</script>\n\n</body>\n</html> ",
        "operation": "htmlToPdf",
        "pdfHeightMm": 120
      },
      "credentials": {
        "customJsApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "9ec01a64-4ba6-4df0-a3fc-8d4dc711af49",
      "name": "Extract from File",
      "type": "n8n-nodes-base.extractFromFile",
      "position": [
        6320,
        1968
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 1.1
    },
    {
      "id": "c05037bf-f6cb-4238-b45d-17f7bd3c3304",
      "name": "Aggregate",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        6512,
        1968
      ],
      "parameters": {
        "options": {},
        "aggregate": "aggregateAllItemData"
      },
      "typeVersion": 1
    },
    {
      "id": "64a36470-99ac-45a7-86c5-6d40ca508615",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        5856,
        2096
      ],
      "parameters": {
        "rule": {
          "interval": [
            {}
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "95630953-76c3-4af6-80d9-4714c17715df",
      "name": "Upsert HTML Page",
      "type": "@custom-js/n8n-nodes-pdf-toolkit-v2.pdfToolkit",
      "position": [
        7200,
        1968
      ],
      "parameters": {
        "pageName": "KPI Dashboard",
        "resource": "page",
        "operation": "upsert",
        "htmlContent": "={{ $('HTML').item.json.html }}"
      },
      "credentials": {
        "customJsApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "HTML": {
      "main": [
        [
          {
            "node": "Upsert HTML Page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate": {
      "main": [
        [
          {
            "node": "HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Get Data From Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upsert HTML Page": {
      "main": [
        [
          {
            "node": "Convert HTML to PDF",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract from File": {
      "main": [
        [
          {
            "node": "Aggregate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Data From Sheet": {
      "main": [
        [
          {
            "node": "Extract from File",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When clicking \u2018Execute workflow\u2019": {
      "main": [
        [
          {
            "node": "Get Data From Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}