{
  "id": "",
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "name": "Weekly Jira Report",
  "tags": [],
  "nodes": [
    {
      "id": "590d7a1e-a79d-4e27-bc96-6a2fc07709a6",
      "name": "Label Updated3",
      "type": "n8n-nodes-base.code",
      "position": [
        1616,
        5840
      ],
      "parameters": {
        "jsCode": "// Label every item from this Jira query with _type = 'updated'\n// Aggregate reads this field \u2014 no guessing, no re-classification.\nreturn $input.all().map(item => ({\n  json: { ...item.json, _type: 'updated' }\n}));"
      },
      "typeVersion": 2
    },
    {
      "id": "f9081239-6fcb-462b-aea2-293f6463d48b",
      "name": "Label Created3",
      "type": "n8n-nodes-base.code",
      "position": [
        1616,
        6032
      ],
      "parameters": {
        "jsCode": "// Label every item from this Jira query with _type = 'created'\n// Aggregate reads this field \u2014 no guessing, no re-classification.\nreturn $input.all().map(item => ({\n  json: { ...item.json, _type: 'created' }\n}));"
      },
      "typeVersion": 2
    },
    {
      "id": "be16c79f-ecb8-4233-ad04-fa1cb1d639c5",
      "name": "Label Completed3",
      "type": "n8n-nodes-base.code",
      "position": [
        1616,
        6208
      ],
      "parameters": {
        "jsCode": "// Label every item from this Jira query with _type = 'completed'\n// Aggregate reads this field \u2014 no guessing, no re-classification.\nreturn $input.all().map(item => ({\n  json: { ...item.json, _type: 'completed' }\n}));"
      },
      "typeVersion": 2
    },
    {
      "id": "98ee8def-8041-4eb2-88b0-2323fdb54ceb",
      "name": "Label Due Soon3",
      "type": "n8n-nodes-base.code",
      "position": [
        1616,
        6400
      ],
      "parameters": {
        "jsCode": "// Label every item from this Jira query with _type = 'dueSoon'\n// Aggregate reads this field \u2014 no guessing, no re-classification.\nreturn $input.all().map(item => ({\n  json: { ...item.json, _type: 'dueSoon' }\n}));"
      },
      "typeVersion": 2
    },
    {
      "id": "382ee5d5-ed15-4659-a325-ed81ee03d144",
      "name": "Label Epics3",
      "type": "n8n-nodes-base.code",
      "position": [
        1616,
        6560
      ],
      "parameters": {
        "jsCode": "// Label every item from this Jira query with _type = 'epics'\n// Aggregate reads this field \u2014 no guessing, no re-classification.\nreturn $input.all().map(item => ({\n  json: { ...item.json, _type: 'epics' }\n}));"
      },
      "typeVersion": 2
    },
    {
      "id": "23665390-c444-4cf5-bc05-4207df9cdf5b",
      "name": "Label All Issues3",
      "type": "n8n-nodes-base.code",
      "position": [
        1616,
        6736
      ],
      "parameters": {
        "jsCode": "// Label every item from this Jira query with _type = 'allIssues'\n// Aggregate reads this field \u2014 no guessing, no re-classification.\nreturn $input.all().map(item => ({\n  json: { ...item.json, _type: 'allIssues' }\n}));"
      },
      "typeVersion": 2
    },
    {
      "id": "857590ff-eab4-4e8d-8d19-4eb52b6ac84e",
      "name": "Sticky Note19",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -528,
        5184
      ],
      "parameters": {
        "width": 960,
        "height": 1232,
        "content": "## What it is ? [ WEEKLY AUTOMATED REPORTING SYSTEM \ud83e\udde0 ]\n\n### \ud83d\udcca An automated system that monitors the team's Jira project every Monday morning, measures progress against the active sprint, and delivers a structured report to stakeholders \u2014 with no manual effort required.\n\n\n## What problem it solves ?\n\n### Sprint progress is invisible until someone manually compiles it. By then it is too late to act. Blocked work goes unnoticed. Overdue issues accumulate quietly. Team capacity is unbalanced without anyone realising. This system makes sprint health visible every week, automatically, before problems escalate.\n\n\n## How it works ?\n\n### Every Monday at ??:?? the system runs automatically. It pulls live data from Jira, measures what happened over the past 7 days, and produces three outputs simultaneously:\n\n- An **HTML report**  + **PDF Attachement** sent by email to the team\n- A **Markdown version** for documentation and archiving\n- A set of **structured records** saved to a database for historical analysis\n\n> ### The entire process takes under one minute and requires no human input.\n\n\n## Multi-Project support\n\n### The system runs one instance per Jira project. All instances share the same database, so cross-project analysis is available out of the box. Adding a new project requires changing a single configuration value (**CONFIGURATION NODE**) \u2014 everything else adapts automatically\n\n\n## Historical Analysis\n\n### Every report is saved to a project shared database. Over time this builds a searchable record that answers questions no single report can:\n\n- Is the team's velocity improving sprint over sprint?\n- Is lead time decreasing as the team matures?\n- Which team member consistently resolves issues fastest?\n- Which epics have stalled across multiple weeks?\n- How does this project compare to others running in parallel?\n\n> ### This turns weekly snapshots into a continuous picture of team performance\n\n\n## JIRA SPACE CONFIGURATION COMPATIBILITY\n\n### Link to Documentation: https://www.notion.so/JIRA-COMPATIBILITY-GUIDE-335f6ec488f68037853adb1722711cae?source=copy_link \ud83d\udd0e\n\n\n## \ud83d\udce9 Contact / Questions / Custom Requests\n\n### Workflow created by\n**aixautomation**  \n- Email: aixautomation01@gmail.com  \n- Website: https://www.aixautomation.tech  \n\n### Need help?\nIf you have any questions about this workflow, need support setting it up, or want to request a custom workflow, feel free to get in touch."
      },
      "typeVersion": 1
    },
    {
      "id": "d549de26-63c6-4d8b-a8e0-1c9f2139220c",
      "name": "Sticky Note21",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        864,
        5696
      ],
      "parameters": {
        "color": 7,
        "width": 1232,
        "height": 1456,
        "content": "## PREPARE DATA ( DATA CONFIGURATION / JIRA ISSUES ) \ud83e\udd16"
      },
      "typeVersion": 1
    },
    {
      "id": "e9712f61-25e8-48c8-b5fa-d6f046483c87",
      "name": "Sticky Note22",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        544,
        5456
      ],
      "parameters": {
        "color": 7,
        "width": 272,
        "height": 448,
        "content": "## CREATE TABLES FOR FIRST TIME RUNNING WORKFLOW 1\ufe0f\u20e3"
      },
      "typeVersion": 1
    },
    {
      "id": "5e54e6de-37e6-470b-a5c3-42c6659822d3",
      "name": "Sticky Note23",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4976,
        6176
      ],
      "parameters": {
        "color": 7,
        "width": 2192,
        "height": 928,
        "content": "## STORE IN DATABASE FOR LATER ANALYTICS WITH (PowerBI, MetaBase...) \ud83d\udcca"
      },
      "typeVersion": 1
    },
    {
      "id": "f20fc7d1-22ae-4096-a51a-cf9c64114cbc",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        864,
        5824
      ],
      "parameters": {
        "color": 3,
        "width": 336,
        "height": 352,
        "content": "## PROJECT CONFIG \u2014 change only this NODE \n### Variables to change:\n(**Project Key**, **Jira Domain**, **Email To**) "
      },
      "typeVersion": 1
    },
    {
      "id": "4bba9917-977e-4e8e-be19-a54e38f44552",
      "name": "Sticky Note25",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2464,
        5952
      ],
      "parameters": {
        "color": 7,
        "width": 1824,
        "height": 1184,
        "content": "## BUILDING BLOCK (Email, Pdf, Markdown) \u2692\ufe0f"
      },
      "typeVersion": 1
    },
    {
      "id": "e5c47e57-0022-484d-a3cf-bf04fa1c5919",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -192,
        4672
      ],
      "parameters": {
        "width": 368,
        "height": 320,
        "content": "## Manual testing"
      },
      "typeVersion": 1
    },
    {
      "id": "af544d3e-3e6f-48d7-9a04-17cfe06a7278",
      "name": "Prepare Assignee Rows1",
      "type": "n8n-nodes-base.code",
      "position": [
        5920,
        6528
      ],
      "parameters": {
        "jsCode": "// Prepare Assignee KPI rows \u2014 reads purely from $input\nconst d = $input.first().json;\nconst rows = (d.kpis?.assigneeKPIs || []);\nif (rows.length === 0) return [{ json: { skipped: true } }];\nreturn rows.map(a => ({ json: {\n    report_id:          d.id,\n    project_key:        d.project_key,\n    project_short:      d.project_short,\n    sprint_name:        d.sprint_name,\n    week_start:         d.week_start,\n    assignee:           a.assignee,\n    total_issues:       a.total          ?? 0,\n    done:               a.done           ?? 0,\n    in_progress:        a.inProgress     ?? 0,\n    todo:               a.toDo           ?? 0,\n    blocked:            a.blocked        ?? 0,\n    story_points_done:  a.spDone         ?? 0,\n    story_points_total: a.storyPoints    ?? 0,\n    avg_lead_time:      a.avgLeadTime    ?? null,\n    completion_rate:    a.completionRate ?? 0,\n}}));"
      },
      "typeVersion": 2
    },
    {
      "id": "730d7a1f-1ab9-4ede-8919-04320afce844",
      "name": "Prepare Epic Rows1",
      "type": "n8n-nodes-base.code",
      "position": [
        5920,
        6784
      ],
      "parameters": {
        "jsCode": "// Prepare Epic Snapshot rows \u2014 reads purely from $input\nconst d = $input.first().json;\nconst epics = d.epicProgress || [];\nif (epics.length === 0) return [{ json: { skipped: true } }];\nreturn epics.map(e => ({ json: {\n    report_id:       d.id,\n    project_key:     d.projectKey,\n    sprint_name:     d.sprintName,\n    week_start:      d.weekStart,\n    epic_key:        e.key,\n    epic_name:       e.name,\n    total_issues:    e.total        ?? 0,\n    done_pct:        e.donePct      ?? 0,\n    in_progress_pct: e.inProgPct    ?? 0,\n    todo_pct:        e.todoPct      ?? 0,\n}}));"
      },
      "typeVersion": 2
    },
    {
      "id": "cb84cce3-5c4e-488c-81cb-c1c1c228acef",
      "name": "When clicking \u2018Execute workflow\u2019",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        -48,
        4800
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "c3b2504e-6fd0-464a-b9b7-ef7b4aab08f8",
      "name": "CREATE TABLES",
      "type": "n8n-nodes-base.postgres",
      "position": [
        624,
        5728
      ],
      "parameters": {
        "query": "-- Drop old unique constraints and replace with multi-project safe ones\n\nCREATE TABLE IF NOT EXISTS sprint_reports (\n  id                  SERIAL PRIMARY KEY,\n  project_key         TEXT NOT NULL,\n  project_name        TEXT,               -- full name: \"PFE Project: Agent IA...\"\n  project_short       TEXT,               -- display: \"Agent IA Backlog...\"\n  sprint_name         TEXT NOT NULL,\n  week_start          DATE NOT NULL,\n  week_end            DATE NOT NULL,\n  sprint_start        DATE,\n  sprint_end          DATE,\n  sprint_pct_elapsed  INT,\n  sprint_goal         TEXT,\n  total_issues        INT,\n  completed_this_week INT,\n  updated_this_week   INT,\n  created_this_week   INT,\n  due_soon            INT,\n  html_report         TEXT,\n  markdown_report     TEXT,\n  subject_line        TEXT,\n  status_overview     JSONB,\n  priority_breakdown  JSONB,\n  due_soon_issues     JSONB,\n  created_at          TIMESTAMP DEFAULT NOW(),\n\n  -- scoped to project + sprint + week (a sprint can span multiple weeks)\n  UNIQUE (project_key, sprint_name, week_start)\n);\n\nCREATE TABLE IF NOT EXISTS weekly_kpis (\n  id                     SERIAL PRIMARY KEY,\n  report_id              INT REFERENCES sprint_reports(id) ON DELETE CASCADE,\n  project_key            TEXT NOT NULL,\n  sprint_name            TEXT NOT NULL,\n  week_start             DATE NOT NULL,\n  -- velocity\n  story_points_completed INT  DEFAULT 0,\n  story_points_total     INT  DEFAULT 0,\n  story_points_remaining INT  DEFAULT 0,\n  -- completion\n  completion_rate        INT  DEFAULT 0,   -- %\n  throughput_week        INT  DEFAULT 0,\n  throughput_sprint      INT  DEFAULT 0,\n  -- health\n  sprint_health          TEXT,             -- 'On Track' | 'Slight Risk' | 'At Risk' | 'Critical'\n  health_gap             INT  DEFAULT 0,   -- pct points behind target\n  -- lead time\n  avg_lead_time          NUMERIC(5,1),     -- null = no resolved issues\n  min_lead_time          NUMERIC(5,1),\n  max_lead_time          NUMERIC(5,1),\n  -- counts\n  done_count             INT  DEFAULT 0,\n  in_progress_count      INT  DEFAULT 0,\n  todo_count             INT  DEFAULT 0,\n  blocked_count          INT  DEFAULT 0,\n  overdue_count          INT  DEFAULT 0,\n  -- sub-arrays stored as jsonb (not useful to aggregate row-by-row)\n  lead_time_by_priority  JSONB,\n  blocked_list           JSONB,\n  overdue_list           JSONB,\n  created_at             TIMESTAMP DEFAULT NOW(),\n\n  UNIQUE (project_key, sprint_name, week_start)\n);\n\nCREATE TABLE IF NOT EXISTS assignee_kpis (\n  id                  SERIAL PRIMARY KEY,\n  report_id           INT REFERENCES sprint_reports(id) ON DELETE CASCADE,\n  project_key         TEXT NOT NULL,\n  project_short       TEXT,\n  sprint_name         TEXT NOT NULL,\n  week_start          DATE NOT NULL,\n  assignee            TEXT NOT NULL,\n  total_issues        INT  DEFAULT 0,\n  done                INT  DEFAULT 0,\n  in_progress         INT  DEFAULT 0,\n  todo                INT  DEFAULT 0,\n  blocked             INT  DEFAULT 0,\n  story_points_done   INT  DEFAULT 0,\n  story_points_total  INT  DEFAULT 0,\n  avg_lead_time       NUMERIC(5,1),\n  completion_rate     INT  DEFAULT 0,\n  created_at          TIMESTAMP DEFAULT NOW(),\n\n  UNIQUE (project_key, sprint_name, week_start, assignee)\n);\n\nCREATE TABLE IF NOT EXISTS epic_snapshots (\n  id              SERIAL PRIMARY KEY,\n  report_id       INT REFERENCES sprint_reports(id) ON DELETE CASCADE,\n  project_key     TEXT NOT NULL,\n  sprint_name     TEXT NOT NULL,\n  week_start      DATE NOT NULL,\n  epic_key        TEXT NOT NULL,\n  epic_name       TEXT,\n  total_issues    INT  DEFAULT 0,\n  done_pct        INT  DEFAULT 0,\n  in_progress_pct INT  DEFAULT 0,\n  todo_pct        INT  DEFAULT 0,\n  created_at      TIMESTAMP DEFAULT NOW(),\n\n  UNIQUE (project_key, sprint_name, week_start, epic_key)\n);\n\n-- Indexes covering every query pattern to run\nCREATE INDEX ON sprint_reports (project_key, week_start); -- SELECT * FROM sprint_reports WHERE project_key='AIBAP' AND week_start='2026-03-30';\nCREATE INDEX ON sprint_reports (sprint_name); -- SELECT * FROM sprint_reports WHERE sprint_name='Sprint 24';\nCREATE INDEX ON weekly_kpis   (project_key, week_start); -- SELECT * FROM weekly_kpis WHERE project_key='AIBAP' AND week_start='2026-03-30';\nCREATE INDEX ON weekly_kpis   (sprint_name);\nCREATE INDEX ON assignee_kpis (project_key, assignee, week_start);\nCREATE INDEX ON assignee_kpis (assignee);  -- cross-project: \"show me assignee across all projects\", SELECT * FROM assignee_kpis WHERE assignee='John';\nCREATE INDEX ON epic_snapshots(project_key, epic_key, week_start); -- SELECT * FROM epic_snapshots WHERE project_key='AIBAP' AND epic_key='EPIC-123' AND week_start='2026-03-30';",
        "options": {},
        "operation": "executeQuery"
      },
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 2.6,
      "alwaysOutputData": true,
      "waitBetweenTries": 5000
    },
    {
      "id": "b80f62ec-0175-4c35-9f31-d9676927505f",
      "name": "CONFIGURATION NODE",
      "type": "n8n-nodes-base.code",
      "position": [
        944,
        6032
      ],
      "parameters": {
        "jsCode": "// ============================================================\n// PROJECT CONFIG \u2014 change only this block per workflow instance\n// Duplicate this workflow for each Jira project, update below.\n// ============================================================\n\nconst PROJECT_KEY  = 'AIBAP';\nconst JIRA_BASE_URL = 'https://your-domain.atlassian.net';  // \u2190 change to your Jira domain\nconst EMAIL_TO     = 'user@example.com';\n\n// \u2500\u2500 Date range (automatic) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst now          = new Date();\nconst sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);\nconst sevenDaysAhead = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);\n\nconst fmtISO  = (d) => d.toISOString().split('T')[0];\nconst fmtLong = (d) => d.toLocaleDateString('en-GB', {\n  day: 'numeric', month: 'long', year: 'numeric'\n});\n\nreturn [{\n  json: {\n    projectKey:    PROJECT_KEY,\n    jiraBaseUrl:   JIRA_BASE_URL,\n    emailTo:       EMAIL_TO,\n\n    today:         fmtISO(now),\n    sevenDaysAgo:  fmtISO(sevenDaysAgo),\n    sevenDaysAhead:fmtISO(sevenDaysAhead),\n    dateRange:     `${fmtLong(sevenDaysAgo)} \u2014 ${fmtLong(now)}`,\n    weekLabel:     `Week of ${fmtLong(sevenDaysAgo)}`,\n    weekStart:     fmtISO(sevenDaysAgo),\n    weekEnd:       fmtISO(now),\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "69eb136c-ac71-4936-bbfe-4a314bf11e1c",
      "name": "Get Updated Issues (7d)",
      "type": "n8n-nodes-base.jira",
      "position": [
        1376,
        5840
      ],
      "parameters": {
        "options": {
          "jql": "=project = \"{{ $input.first().json.projectKey }}\" AND sprint in openSprints() AND updated >= -7d ORDER BY updated DESC"
        },
        "operation": "getAll",
        "returnAll": true
      },
      "credentials": {
        "jiraSoftwareCloudApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "426a018d-11c8-4586-b9b3-3da7cf01c966",
      "name": "Get Created Issues (7d)",
      "type": "n8n-nodes-base.jira",
      "position": [
        1376,
        6032
      ],
      "parameters": {
        "options": {
          "jql": "=project = \"{{ $input.first().json.projectKey }}\" AND sprint in openSprints() AND created >= -7d"
        },
        "operation": "getAll",
        "returnAll": true
      },
      "credentials": {
        "jiraSoftwareCloudApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "72ad2e90-9db8-4522-80d4-4a1993098e7c",
      "name": "Get Completed Issues (7d)",
      "type": "n8n-nodes-base.jira",
      "position": [
        1376,
        6208
      ],
      "parameters": {
        "options": {
          "jql": "=project = \"{{ $input.first().json.projectKey }}\" AND sprint in openSprints() AND statusCategory in (\"Done\") AND updated >= -7d"
        },
        "operation": "getAll",
        "returnAll": true
      },
      "credentials": {
        "jiraSoftwareCloudApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "a03b8110-5ec1-4092-a58b-fc484b8a84d0",
      "name": "Get Due Soon Issues",
      "type": "n8n-nodes-base.jira",
      "position": [
        1376,
        6400
      ],
      "parameters": {
        "options": {
          "jql": "=project = \"{{ $input.first().json.projectKey }}\" AND sprint in openSprints() AND duedate >= now() AND duedate <= endOfDay(\"+7d\") AND statusCategory != \"Done\""
        },
        "operation": "getAll",
        "returnAll": true
      },
      "credentials": {
        "jiraSoftwareCloudApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "397dead5-76e3-45f6-95b4-b09e598a5f29",
      "name": "Get All Epics",
      "type": "n8n-nodes-base.jira",
      "position": [
        1376,
        6560
      ],
      "parameters": {
        "options": {
          "jql": "=project = \"{{ $input.first().json.projectKey }}\" AND issuetype = Epic"
        },
        "operation": "getAll",
        "returnAll": true
      },
      "credentials": {
        "jiraSoftwareCloudApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "0e27063a-275e-4f65-b487-54623d9f74ef",
      "name": "Get All Issues (Status Overview)",
      "type": "n8n-nodes-base.jira",
      "position": [
        1376,
        6736
      ],
      "parameters": {
        "options": {
          "jql": "=project = \"{{ $input.first().json.projectKey }}\" AND sprint in openSprints()"
        },
        "operation": "getAll",
        "returnAll": true
      },
      "credentials": {
        "jiraSoftwareCloudApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "e785939e-7f04-4f6b-aa8c-e136fc5a82f1",
      "name": "Labels Config",
      "type": "n8n-nodes-base.code",
      "position": [
        1376,
        6960
      ],
      "parameters": {
        "jsCode": "// Stamp the config item with _type = 'config'\nreturn $input.all().map(item => ({\n  json: { ...item.json, _type: 'config' }\n}));"
      },
      "typeVersion": 2
    },
    {
      "id": "91b11225-b274-409d-a82d-ef80a393b102",
      "name": "WAIT_FOR_BODY_ATTACHEMENT",
      "type": "n8n-nodes-base.merge",
      "position": [
        4096,
        6784
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineAll"
      },
      "typeVersion": 3.2
    },
    {
      "id": "78bdce21-c456-46ec-8c35-4d4fd443d8bb",
      "name": "HTML",
      "type": "n8n-nodes-base.html",
      "position": [
        3456,
        6080
      ],
      "parameters": {
        "html": "{{ $input.last().json.html }}"
      },
      "typeVersion": 1.2
    },
    {
      "id": "ad7874c1-ac06-4d6c-9eb4-5b06978b0072",
      "name": "Build Email HTML",
      "type": "n8n-nodes-base.code",
      "position": [
        2944,
        6080
      ],
      "parameters": {
        "jsCode": "// ============================================================\n// BUILD EMAIL HTML \u2014 Slim digest v2 (business-reader restructure)\n// Order: Header \u2192 Alert \u2192 Exec Summary \u2192 Health+Timeline \u2192\n//        3 Key Numbers \u2192 Donut Status \u2192 Blocked Detail \u2192 CTA \u2192 Footer\n// ============================================================\n\nconst d = $input.first().json;\nconst {\n  projectKey, projectName, projectShort,\n  sprintName, sprintGoal, sprintDates, sprintStartFmt, sprintEndFmt,\n  sprintPct, sprintDaysLeft,\n  reportTitle, weekLabel, dateRange,\n  metrics, kpis, statusOverview, dueSoonIssues\n} = d;\n\nconst JIRA_BASE = $('CONFIGURATION NODE').first().json.jiraBaseUrl + \"/browse\";\nconst BRAND     = '#0F3D6E';\nconst BRAND_ACC = '#0052CC';\nconst SURFACE   = '#FFFFFF';\nconst SURFACE2  = '#F4F5F7';\nconst BORDER    = '#DFE1E6';\nconst TEXT1     = '#172B4D';\nconst TEXT2     = '#5E6C84';\nconst TEXT3     = '#8993A4';\n\nconst STATUS_COLOR_MAP = {\n  'Done':'#36B37E','Termin\u00e9':'#36B37E',\n  'In Progress':'#0052CC','En cours':'#0052CC',\n  'In Review':'#6554C0','En r\u00e9vision':'#6554C0','Revue en cours':'#6554C0',\n  'To Do':'#DFE1E6','\u00c0 faire':'#DFE1E6','Nouveau':'#DFE1E6','New':'#DFE1E6',\n  'Blocked':'#BF2600'\n};\n\nconst normalizeStatus = (s) => {\n  if (!s) return 'Unknown';\n  const map = {\n    'Done':'Done','Termin\u00e9':'Done',\n    'In Progress':'In Progress','En cours':'In Progress',\n    'In Review':'In Review','En r\u00e9vision':'In Review','Revue en cours':'In Review',\n    'To Do':'To Do','\u00c0 faire':'To Do','Nouveau':'To Do','New':'To Do',\n    'Blocked':'Blocked',\n  };\n  if (map[s]) return map[s];\n  const sl = s.toLowerCase();\n  if (sl.startsWith('termin\u00e9') || sl.startsWith('termine')) return 'Done';\n  if (sl.startsWith('en cours')) return 'In Progress';\n  if (sl.includes('r\u00e9vision') || sl.includes('review')) return 'In Review';\n  if (sl.startsWith('\u00e0 faire') || sl === 'nouveau' || sl === 'new') return 'To Do';\n  return s;\n};\n\n// \u2500\u2500 Health colors \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst healthColor = {'On Track':'#006644','Slight Risk':'#FF8B00','At Risk':'#BF2600','Critical':'#BF2600'}[kpis.sprintHealth] || TEXT2;\nconst healthBg    = {'On Track':'#E3FCEF','Slight Risk':'#FFFAE6','At Risk':'#FFEBE6','Critical':'#FFEBE6'}[kpis.sprintHealth] || SURFACE2;\nconst healthBorder= {'On Track':'#36B37E','Slight Risk':'#FF8B00','At Risk':'#BF2600','Critical':'#BF2600'}[kpis.sprintHealth] || BORDER;\n\n// \u2500\u2500 Sprint timeline helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst daysLabel = sprintDaysLeft===0 ? 'Sprint ended'\n  : sprintDaysLeft===1 ? '1 day left'\n  : sprintDaysLeft!==null ? `${sprintDaysLeft} days left` : '';\nconst daysColor = sprintDaysLeft===0 ? '#BF2600' : (sprintPct>=80 ? '#FF8B00' : TEXT2);\nconst donePct = metrics.totalIssues > 0\n  ? Math.round((statusOverview.find(s=>s.status==='Done'||s.status==='Termin\u00e9')?.count||0) / metrics.totalIssues * 100) : 0;\nconst barColor = (sprintPct>=90 && donePct<60) ? '#BF2600' : (sprintPct>=70 && donePct<40) ? '#FF8B00' : '#36B37E';\nconst sprintBandFill = Math.max(0, Math.min(100, sprintPct));\n\n// \u2500\u2500 Donut SVG \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst totalIssues = metrics.totalIssues || 1;\nconst R_OUT=54, R_IN=34, CX=64, CY=64, GAP=0.04;\nlet angle = -Math.PI/2;\nconst donutPaths = statusOverview.map(s => {\n  const frac = s.count/totalIssues;\n  const sweep = frac*2*Math.PI - GAP;\n  if (sweep<=0) return '';\n  const sA=angle+GAP/2, eA=sA+sweep;\n  angle += frac*2*Math.PI;\n  const x1=CX+R_OUT*Math.cos(sA),y1=CY+R_OUT*Math.sin(sA);\n  const x2=CX+R_OUT*Math.cos(eA),y2=CY+R_OUT*Math.sin(eA);\n  const x3=CX+R_IN*Math.cos(eA), y3=CY+R_IN*Math.sin(eA);\n  const x4=CX+R_IN*Math.cos(sA), y4=CY+R_IN*Math.sin(sA);\n  const lg=sweep>Math.PI?1:0;\n  const col=STATUS_COLOR_MAP[s.status]||'#8993A4';\n  return `<path d=\"M${x1.toFixed(2)},${y1.toFixed(2)} A${R_OUT},${R_OUT} 0 ${lg},1 ${x2.toFixed(2)},${y2.toFixed(2)} L${x3.toFixed(2)},${y3.toFixed(2)} A${R_IN},${R_IN} 0 ${lg},0 ${x4.toFixed(2)},${y4.toFixed(2)} Z\" fill=\"${col}\"/>`;\n}).join('');\n\nconst donutSvg = `<svg width=\"110\" height=\"110\" viewBox=\"0 0 128 128\" xmlns=\"http://www.w3.org/2000/svg\">\n  ${donutPaths}\n  <text x=\"64\" y=\"58\" text-anchor=\"middle\" font-size=\"22\" font-weight=\"700\" fill=\"${TEXT1}\" font-family=\"-apple-system,Arial,sans-serif\">${metrics.totalIssues}</text>\n  <text x=\"64\" y=\"75\" text-anchor=\"middle\" font-size=\"10\" fill=\"${TEXT2}\" font-family=\"-apple-system,Arial,sans-serif\">issues</text>\n</svg>`;\n\nconst statusLegend = statusOverview.map(s => {\n  const col = STATUS_COLOR_MAP[s.status]||'#8993A4';\n  const pct = Math.round(s.count/totalIssues*100);\n  return `<tr>\n    <td style=\"padding:4px 8px 4px 0;width:12px;\"><div style=\"width:8px;height:8px;border-radius:2px;background:${col};\"></div></td>\n    <td style=\"padding:4px 0;font-size:11px;color:${TEXT1};\">${normalizeStatus(s.status)}</td>\n    <td style=\"padding:4px 0;text-align:right;white-space:nowrap;\">\n      <span style=\"font-size:11px;font-weight:700;color:${TEXT1};\">${s.count}</span>\n      <span style=\"font-size:10px;color:${TEXT3};margin-left:2px;\">(${pct}%)</span>\n    </td>\n  </tr>`;\n}).join('');\n\n// \u2500\u2500 Derived values \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst hasRisks   = kpis.blockedCount > 0 || kpis.overdueCount > 0;\nconst isComplete = sprintDaysLeft === 0;\n\n// Health reason hint \u2014 tells manager WHY there's a risk\nconst healthReason = (() => {\n  if (kpis.blockedCount > 0 && kpis.overdueCount > 0)\n    return `${kpis.blockedCount} blocked \u00b7 ${kpis.overdueCount} overdue`;\n  if (kpis.blockedCount > 0)\n    return `${kpis.blockedCount} issue${kpis.blockedCount>1?'s':''} blocked`;\n  if (kpis.overdueCount > 0)\n    return `${kpis.overdueCount} item${kpis.overdueCount>1?'s':''} overdue`;\n  return kpis.healthGap > 0 ? `${kpis.healthGap}% behind target` : 'Ahead of schedule';\n})();\n\n// Executive summary \u2014 one sentence a manager can act on\nconst execSummary = (() => {\n  const parts = [];\n  parts.push(`Sprint is <strong>${isComplete ? 'complete' : sprintPct + '% elapsed'}</strong> with <strong>${kpis.completionRate}% of work delivered</strong> (${kpis.doneCount} of ${metrics.totalIssues} issues done).`);\n  if (kpis.blockedCount > 0)\n    parts.push(`<strong style=\"color:#BF2600;\">${kpis.blockedCount} issue${kpis.blockedCount>1?'s are':' is'} blocked</strong> and require${kpis.blockedCount===1?'s':''} immediate attention.`);\n  if (kpis.overdueCount > 0)\n    parts.push(`<strong style=\"color:#BF2600;\">${kpis.overdueCount} item${kpis.overdueCount>1?'s are':' is'} overdue</strong>.`);\n  if (!hasRisks)\n    parts.push('No blockers or overdue items. \ud83d\udfe2');\n  return parts.join(' ');\n})();\n\n// \u2500\u2500 1. Alert banner (risks only) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst alertBanner = hasRisks ? `\n<tr><td style=\"background:#FFF3CD;border-left:4px solid #FF8B00;border-right:1px solid ${BORDER};padding:11px 20px;\">\n  <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"><tr valign=\"middle\">\n    <td style=\"font-size:10px;font-weight:700;color:#7A4F00;text-transform:uppercase;letter-spacing:0.07em;width:1%;white-space:nowrap;padding-right:12px;\">\u26a0 Action Required</td>\n    <td style=\"font-size:11px;color:#5C3D00;line-height:1.5;\">\n      ${(kpis.blockedList||[]).map(b=>`<strong>${b.key}</strong> blocked ${b.daysBlocked}d \u2014 ${b.assignee}`).join(' &nbsp;\u00b7&nbsp; ')}\n      ${(kpis.overdueList||[]).length > 0 ? ' &nbsp;\u00b7&nbsp; ' + kpis.overdueList.map(o=>`<strong>${o.key}</strong> overdue ${o.daysOverdue}d`).join(', ') : ''}\n    </td>\n  </tr></table>\n</td></tr>` : '';\n\n// \u2500\u2500 2. Exec summary bar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst execSummaryRow = `\n<tr><td style=\"background:${SURFACE2};border-left:1px solid ${BORDER};border-right:1px solid ${BORDER};border-top:1px solid ${BORDER};padding:12px 24px;\">\n  <div style=\"font-size:12px;color:${TEXT1};line-height:1.7;\">${execSummary}</div>\n</td></tr>`;\n\n// \u2500\u2500 3. Sprint Health + Timeline combined \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst healthAndTimeline = `\n<tr><td style=\"background:${healthBg};border-left:1px solid ${BORDER};border-right:1px solid ${BORDER};border-top:4px solid ${healthBorder};padding:16px 30px;\">\n  <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"><tr valign=\"middle\">\n\n    <!-- Health status -->\n    <td style=\"padding-right:20px;border-right:1px solid rgba(0,0,0,0.08);white-space:nowrap;\">\n      <div style=\"font-size:9px;font-weight:700;color:${healthColor};text-transform:uppercase;letter-spacing:0.08em;margin-bottom:4px;\">Sprint Health</div>\n      <div style=\"font-size:20px;font-weight:700;color:${healthColor};line-height:1;\">${kpis.sprintHealth}</div>\n      <div style=\"font-size:10px;color:${TEXT2};margin-top:4px;\">${healthReason}</div>\n    </td>\n\n    <!-- Timeline bar -->\n    <td style=\"padding-left:20px;\">\n      <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">\n        <tr>\n          <td style=\"padding-bottom:5px;font-size:9px;font-weight:700;letter-spacing:0.07em;color:${TEXT3};text-transform:uppercase;\">Sprint Timeline</td>\n          <td style=\"padding-bottom:5px;text-align:right;font-size:9px;font-weight:700;color:${daysColor};\">${daysLabel}</td>\n        </tr>\n        <tr><td colspan=\"2\">\n          <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:#DFE1E6;border-radius:4px;height:8px;\">\n            <tr>\n              <td width=\"${sprintBandFill}%\" style=\"background:${barColor};height:8px;border-radius:4px;font-size:0;line-height:0;\">&nbsp;</td>\n              <td style=\"height:8px;font-size:0;line-height:0;\">&nbsp;</td>\n            </tr>\n          </table>\n        </td></tr>\n        <tr>\n          <td style=\"padding-top:4px;font-size:9px;color:${TEXT3};\">${sprintStartFmt}</td>\n          <td style=\"padding-top:4px;text-align:right;font-size:9px;color:${TEXT3};\">${sprintEndFmt}</td>\n        </tr>\n      </table>\n    </td>\n\n  </tr></table>\n</td></tr>`;\n\n// \u2500\u2500 4. Unified metrics section \u2014 all numbers in one block \u2500\u2500\u2500\u2500\u2500\n// Row 1 (primary): Completion \u00b7 Blocked \u00b7 Due Soon \u2014 large, action-oriented\n// Row 2 (secondary): Completed \u00b7 Updated \u00b7 Created \u00b7 Avg Delivery \u2014 compact week context\nconst unifiedMetrics = `\n<tr><td style=\"background:${SURFACE};border-left:1px solid ${BORDER};border-right:1px solid ${BORDER};border-top:1px solid ${BORDER};padding:18px 24px 14px;\">\n\n  <!-- Section label -->\n  <div style=\"font-size:9px;font-weight:700;color:${TEXT3};text-transform:uppercase;letter-spacing:0.08em;margin-bottom:14px;\">Sprint at a Glance</div>\n\n  <!-- Row 1: 3 primary KPIs \u2014 large numbers, colour-coded by urgency -->\n  <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"table-layout:fixed;margin-bottom:0;\"><tr valign=\"top\">\n\n    <td style=\"padding-right:5px;\">\n      <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"><tr>\n        <td style=\"border-left:3px solid #36B37E;background:#F2FBF6;border-radius:0 5px 5px 0;padding:11px 13px;\">\n          <div style=\"font-size:9px;font-weight:700;color:#006644;text-transform:uppercase;letter-spacing:0.08em;margin-bottom:4px;\">Completion</div>\n          <div style=\"font-size:26px;font-weight:700;color:#006644;line-height:1;\">${kpis.completionRate}%</div>\n          <div style=\"font-size:10px;color:${TEXT3};margin-top:4px;\">${kpis.doneCount} of ${metrics.totalIssues} done</div>\n        </td>\n      </tr></table>\n    </td>\n\n    <td style=\"padding:0 5px;\">\n      <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"><tr>\n        <td style=\"border-left:3px solid ${kpis.blockedCount>0?'#BF2600':BORDER};background:${kpis.blockedCount>0?'#FFEBE6':SURFACE2};border-radius:0 5px 5px 0;padding:11px 13px;\">\n          <div style=\"font-size:9px;font-weight:700;color:${kpis.blockedCount>0?'#BF2600':TEXT3};text-transform:uppercase;letter-spacing:0.08em;margin-bottom:4px;\">Blocked</div>\n          <div style=\"font-size:26px;font-weight:700;color:${kpis.blockedCount>0?'#BF2600':TEXT3};line-height:1;\">${kpis.blockedCount}</div>\n          <div style=\"font-size:10px;color:${TEXT3};margin-top:4px;\">${kpis.blockedCount>0?'need attention':'no blockers \ud83d\udfe2'}</div>\n        </td>\n      </tr></table>\n    </td>\n\n    <td style=\"padding-left:5px;\">\n      <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"><tr>\n        <td style=\"border-left:3px solid ${metrics.dueSoon>0?'#FF8B00':BORDER};background:${metrics.dueSoon>0?'#FFFAE6':SURFACE2};border-radius:0 5px 5px 0;padding:11px 13px;\">\n          <div style=\"font-size:9px;font-weight:700;color:${metrics.dueSoon>0?'#FF8B00':TEXT3};text-transform:uppercase;letter-spacing:0.08em;margin-bottom:4px;\">Due Soon</div>\n          <div style=\"font-size:26px;font-weight:700;color:${metrics.dueSoon>0?'#FF8B00':TEXT3};line-height:1;\">${metrics.dueSoon}</div>\n          <div style=\"font-size:10px;color:${TEXT3};margin-top:4px;\">next 7 days</div>\n        </td>\n      </tr></table>\n    </td>\n\n  </tr></table>\n\n  <!-- Divider with label -->\n  <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin:12px 0 10px;\">\n    <tr valign=\"middle\">\n      <td style=\"border-top:1px solid ${BORDER};font-size:0;\">&nbsp;</td>\n      <td style=\"white-space:nowrap;padding:0 10px;font-size:9px;font-weight:700;color:${TEXT3};text-transform:uppercase;letter-spacing:0.07em;\">This Week</td>\n      <td style=\"border-top:1px solid ${BORDER};font-size:0;\">&nbsp;</td>\n    </tr>\n  </table>\n\n  <!-- Row 2: 4 weekly numbers \u2014 compact, equal columns -->\n  <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"table-layout:fixed;\"><tr valign=\"middle\">\n\n    <td style=\"text-align:center;padding:8px 4px;border-right:1px solid ${BORDER};\">\n      <div style=\"font-size:20px;font-weight:700;color:#006644;line-height:1;\">${metrics.completed}</div>\n      <div style=\"font-size:9px;font-weight:700;color:${TEXT3};text-transform:uppercase;letter-spacing:0.06em;margin-top:4px;\">Completed</div>\n    </td>\n\n    <td style=\"text-align:center;padding:8px 4px;border-right:1px solid ${BORDER};\">\n      <div style=\"font-size:20px;font-weight:700;color:#0052CC;line-height:1;\">${metrics.updated}</div>\n      <div style=\"font-size:9px;font-weight:700;color:${TEXT3};text-transform:uppercase;letter-spacing:0.06em;margin-top:4px;\">Updated</div>\n    </td>\n\n    <td style=\"text-align:center;padding:8px 4px;border-right:1px solid ${BORDER};\">\n      <div style=\"font-size:20px;font-weight:700;color:#6554C0;line-height:1;\">${metrics.created}</div>\n      <div style=\"font-size:9px;font-weight:700;color:${TEXT3};text-transform:uppercase;letter-spacing:0.06em;margin-top:4px;\">Created</div>\n    </td>\n\n    <td style=\"text-align:center;padding:8px 4px;\">\n      <div style=\"font-size:20px;font-weight:700;color:#6554C0;line-height:1;\">${kpis.avgLeadTime !== null ? kpis.avgLeadTime+'d' : '\u2014'}</div>\n      <div style=\"font-size:9px;font-weight:700;color:${TEXT3};text-transform:uppercase;letter-spacing:0.06em;margin-top:4px;\">Avg Delivery</div>\n    </td>\n\n  </tr></table>\n\n</td></tr>`;\n\n// \u2500\u2500 5. Sprint Status donut \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst donutSection = `\n<tr><td style=\"background:${SURFACE};border-left:1px solid ${BORDER};border-right:1px solid ${BORDER};border-top:1px solid ${BORDER};padding:18px 30px;\">\n  <div style=\"font-size:11px;font-weight:700;color:${TEXT3};text-transform:uppercase;letter-spacing:0.07em;margin-bottom:14px;\">Issue Breakdown</div>\n  <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"><tr valign=\"middle\">\n    <td width=\"120\" style=\"text-align:center;\">${donutSvg}</td>\n    <td style=\"padding-left:20px;\"><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">${statusLegend}</table></td>\n  </tr></table>\n</td></tr>`;\n\n// \u2500\u2500 6. Blocked detail (no duplication \u2014 replaces standalone card) \u2500\u2500\nconst blockedDetail = (kpis.blockedList||[]).length === 0 ? '' : `\n<tr><td style=\"background:#FFEBE6;border-left:4px solid #BF2600;border-right:1px solid ${BORDER};border-top:1px solid ${BORDER};padding:16px 30px;\">\n  <div style=\"font-size:11px;font-weight:700;color:#BF2600;text-transform:uppercase;letter-spacing:0.07em;margin-bottom:12px;\">\n    Blocked Items \u2014 Immediate Attention Required\n  </div>\n  ${(kpis.blockedList||[]).slice(0,3).map(b=>`\n  <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin-bottom:8px;\"><tr valign=\"top\">\n    <td style=\"width:1%;white-space:nowrap;padding-right:10px;\">\n      <a href=\"${JIRA_BASE}/${b.key}\" style=\"font-size:11px;font-weight:700;color:${BRAND_ACC};text-decoration:none;\">${b.key}</a>\n    </td>\n    <td>\n      <div style=\"font-size:12px;color:${TEXT1};font-weight:500;line-height:1.4;\">${b.summary}</div>\n      <div style=\"font-size:10px;color:${TEXT3};margin-top:2px;\">\n        ${b.assignee}${b.daysBlocked!==null?` \u00b7 Blocked for <strong style=\"color:#BF2600;\">${b.daysBlocked} days</strong>`:''}\n        ${b.priority ? ` \u00b7 ${b.priority}` : ''}\n      </div>\n    </td>\n  </tr></table>`).join('')}\n</td></tr>`;\n\n// \u2500\u2500 7. PDF CTA \u2014 prominent \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst ctaRow = `\n<tr><td style=\"background:${BRAND};border-left:1px solid ${BORDER};border-right:1px solid ${BORDER};border-top:1px solid ${BORDER};padding:16px 30px;\">\n  <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"><tr valign=\"middle\">\n    <td>\n      <div style=\"font-size:12px;font-weight:700;color:#FFFFFF;margin-bottom:3px;\">\ud83d\udcce Full Report Attached</div>\n      <div style=\"font-size:10px;color:rgba(255,255,255,0.55);\">Includes activity log, theme progress, team breakdown &amp; delivery time analysis</div>\n    </td>\n    <td style=\"text-align:right;white-space:nowrap;padding-left:16px;\">\n      <div style=\"display:inline-block;background:rgba(255,255,255,0.15);border:1px solid rgba(255,255,255,0.3);border-radius:4px;padding:7px 16px;font-size:11px;font-weight:700;color:#FFFFFF;\">View PDF \u2192</div>\n    </td>\n  </tr></table>\n</td></tr>`;\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// ASSEMBLE\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nconst html = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<title>${reportTitle}</title>\n</head>\n<body style=\"margin:0;padding:0;background:#E4E8EF;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Helvetica Neue',Arial,sans-serif;\">\n\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:#E4E8EF;padding:28px 0;\">\n<tr><td align=\"center\">\n<table width=\"620\" cellpadding=\"0\" cellspacing=\"0\" style=\"max-width:620px;width:100%;\">\n\n<!-- 1. HEADER -->\n<tr><td style=\"background:${BRAND};border-radius:8px 8px 0 0;padding:26px 30px;\">\n  <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"><tr valign=\"top\">\n    <td style=\"padding-right:16px;\">\n      <div style=\"font-size:9px;font-weight:700;letter-spacing:0.1em;color:rgba(255,255,255,0.4);text-transform:uppercase;margin-bottom:8px;\">${projectKey} &nbsp;/&nbsp; Weekly Status Report</div>\n      <div style=\"font-size:19px;font-weight:700;color:#FFFFFF;line-height:1.3;margin-bottom:5px;\">${projectShort}</div>\n      <div style=\"font-size:11px;color:rgba(255,255,255,0.5);\">${dateRange}</div>\n      ${sprintGoal?`<div style=\"font-size:11px;color:rgba(255,255,255,0.45);margin-top:8px;border-left:2px solid rgba(255,255,255,0.2);padding-left:8px;font-style:italic;\">Goal: ${sprintGoal}</div>`:''}\n    </td>\n    <td style=\"width:170px;vertical-align:top;\">\n      <table width=\"170\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:rgba(255,255,255,0.11);border:1px solid rgba(255,255,255,0.15);border-radius:6px;\">\n      <tr><td style=\"padding:12px 14px;\">\n        <div style=\"font-size:9px;font-weight:700;letter-spacing:0.09em;color:rgba(255,255,255,0.4);text-transform:uppercase;margin-bottom:5px;\">Active Sprint</div>\n        <div style=\"font-size:13px;font-weight:700;color:#FFFFFF;margin-bottom:2px;\">${sprintName}</div>\n        <div style=\"font-size:10px;color:rgba(255,255,255,0.5);margin-bottom:10px;\">${sprintDates}</div>\n        <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"><tr>\n          <td style=\"font-size:9px;color:rgba(255,255,255,0.4);\">${sprintPct}% elapsed</td>\n          <td style=\"text-align:right;font-size:9px;font-weight:700;color:${sprintDaysLeft===0?'#FF8B00':'rgba(255,255,255,0.6)'};\">${daysLabel}</td>\n        </tr></table>\n      </td></tr></table>\n    </td>\n  </tr></table>\n</td></tr>\n\n<!-- 2. ALERT BANNER (risks only) -->\n${alertBanner}\n\n<!-- 3. EXECUTIVE SUMMARY -->\n${execSummaryRow}\n\n<!-- 4. SPRINT HEALTH + TIMELINE combined -->\n${healthAndTimeline}\n\n<!-- 5. UNIFIED METRICS \u2014 all numbers in one section -->\n${unifiedMetrics}\n\n<!-- 6. ISSUE BREAKDOWN DONUT -->\n${donutSection}\n\n<!-- 7. BLOCKED DETAIL (only if blockers exist) -->\n${blockedDetail}\n\n<!-- 8. PDF CTA \u2014 prominent -->\n${ctaRow}\n\n<!-- BOTTOM PADDING -->\n<tr><td style=\"background:${SURFACE};border-left:1px solid ${BORDER};border-right:1px solid ${BORDER};padding-bottom:16px;\"></td></tr>\n\n<!-- 10. FOOTER -->\n<tr><td style=\"background:${BRAND};border-radius:0 0 8px 8px;padding:16px 30px;\">\n  <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"><tr valign=\"top\">\n    <td>\n      <div style=\"font-size:11px;color:rgba(255,255,255,0.85);\">Auto-generated by the <strong style=\"color:#FFFFFF;\">n8n Reporting Agent</strong> for <strong style=\"color:#FFFFFF;\">${projectName}</strong></div>\n      <div style=\"font-size:10px;color:rgba(255,255,255,0.38);margin-top:4px;\">${sprintName}&nbsp;\u00b7&nbsp;${sprintDates}&nbsp;\u00b7&nbsp;Jira \u2014 IG Green Team</div>\n    </td>\n    <td style=\"text-align:right;padding-left:16px;white-space:nowrap;\">\n      <div style=\"font-size:10px;color:rgba(255,255,255,0.3);\">Do not reply&nbsp;\u00b7&nbsp;Sent Mondays 08:00</div>\n    </td>\n  </tr></table>\n</td></tr>\n\n</table>\n</td></tr>\n</table>\n</body>\n</html>`;\n\nreturn [{ json: {\n  html,\n  subject: `\ud83d\udcca ${sprintName} \u2014 Weekly Report ${dateRange} \u2014 ${projectShort}`,\n  reportTitle\n} }];"
      },
      "typeVersion": 2
    },
    {
      "id": "018ab1c3-df37-4360-addf-72f4dac75551",
      "name": "Build Full HTML",
      "type": "n8n-nodes-base.code",
      "position": [
        2944,
        6288
      ],
      "parameters": {
        "jsCode": "// ============================================================\n// BUILD HTML REPORT \u2014 Sprint Edition v8\n// ============================================================\n\nconst d = $input.first().json;\nconst {\n  projectKey, projectName, projectShort,\n  sprintName, sprintGoal, sprintDates, sprintStartFmt, sprintEndFmt,\n  sprintPct, sprintDaysLeft,\n  reportTitle, weekLabel, dateRange,\n  metrics, kpis, statusOverview, priorityBreakdown,\n  teamWorkload, epicProgress, recentActivity,\n  dueSoonIssues\n} = d;\n\nconst JIRA_BASE = $('CONFIGURATION NODE').first().json.jiraBaseUrl + \"/browse\";\n\n// \u2500\u2500 Design tokens \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst BRAND     = '#0F3D6E';\nconst BRAND_ACC = '#0052CC';\nconst SURFACE   = '#FFFFFF';\nconst SURFACE2  = '#F4F5F7';\nconst BORDER    = '#DFE1E6';\nconst TEXT1     = '#172B4D';\nconst TEXT2     = '#5E6C84';\nconst TEXT3     = '#8993A4';\n\n// \u2500\u2500 Status helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst statusColor = (s) => ({\n  'Done':'#006644','Termin\u00e9':'#006644',\n  'In Progress':'#0052CC','En cours':'#0052CC',\n  'In Review':'#6554C0','En r\u00e9vision':'#6554C0','Revue en cours':'#6554C0',\n  'To Do':'#42526E','\u00c0 faire':'#42526E','Nouveau':'#42526E','New':'#42526E',\n  'Blocked':'#BF2600'\n})[s] || '#42526E';\n\nconst statusBg = (s) => ({\n  'Done':'#E3FCEF','Termin\u00e9':'#E3FCEF',\n  'In Progress':'#DEEBFF','En cours':'#DEEBFF',\n  'In Review':'#EAE6FF','En r\u00e9vision':'#EAE6FF','Revue en cours':'#EAE6FF',\n  'To Do':'#F4F5F7','\u00c0 faire':'#F4F5F7','Nouveau':'#F4F5F7','New':'#F4F5F7',\n  'Blocked':'#FFEBE6'\n})[s] || '#F4F5F7';\n\nconst STATUS_COLOR_MAP = {\n  'Done':'#36B37E','Termin\u00e9':'#36B37E',\n  'In Progress':'#0052CC','En cours':'#0052CC',\n  'In Review':'#6554C0','En r\u00e9vision':'#6554C0','Revue en cours':'#6554C0',\n  'To Do':'#DFE1E6','\u00c0 faire':'#DFE1E6','Nouveau':'#DFE1E6','New':'#DFE1E6',\n  'Blocked':'#BF2600'\n};\n\nconst priorityColor = (p) => ({\n  'Highest':'#BF2600','High':'#FF5630',\n  'Medium':'#FF8B00','Low':'#36B37E','Lowest':'#8993A4'\n})[p] || '#8993A4';\n\nconst pDot = (p) =>\n  `<span style=\"display:inline-block;width:8px;height:8px;border-radius:50%;background:${priorityColor(p)};margin-right:5px;vertical-align:middle;\"></span>`;\n\n// \u2500\u2500 Sprint labels \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst donePct = metrics.totalIssues > 0\n  ? Math.round((statusOverview.find(s => s.status==='Done'||s.status==='Termin\u00e9')?.count||0) / metrics.totalIssues * 100)\n  : 0;\nconst barColor = (sprintPct>=90 && donePct<60) ? '#BF2600'\n  : (sprintPct>=70 && donePct<40) ? '#FF8B00' : '#36B37E';\nconst daysLabel = sprintDaysLeft===0 ? 'Sprint ended'\n  : sprintDaysLeft===1 ? '1 day left'\n  : sprintDaysLeft!==null ? `${sprintDaysLeft} days left` : '';\nconst daysColor = sprintDaysLeft===0 ? '#BF2600' : (sprintPct>=80 ? '#FF8B00' : TEXT2);\n\n// \u2500\u2500 Donut SVG \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst totalIssues = metrics.totalIssues || 1;\nconst R_OUT=54, R_IN=34, CX=64, CY=64, GAP=0.04;\nlet angle = -Math.PI/2;\nconst donutPaths = statusOverview.map((s,idx) => {\n  const frac = s.count/totalIssues;\n  const sweep = frac*2*Math.PI - GAP;\n  if (sweep<=0) return '';\n  const sA=angle+GAP/2, eA=sA+sweep;\n  angle += frac*2*Math.PI;\n  const x1=CX+R_OUT*Math.cos(sA), y1=CY+R_OUT*Math.sin(sA);\n  const x2=CX+R_OUT*Math.cos(eA), y2=CY+R_OUT*Math.sin(eA);\n  const x3=CX+R_IN*Math.cos(eA),  y3=CY+R_IN*Math.sin(eA);\n  const x4=CX+R_IN*Math.cos(sA),  y4=CY+R_IN*Math.sin(sA);\n  const lg=sweep>Math.PI?1:0;\n  const col=STATUS_COLOR_MAP[s.status]||'#8993A4';\n  return `<path d=\"M${x1.toFixed(2)},${y1.toFixed(2)} A${R_OUT},${R_OUT} 0 ${lg},1 ${x2.toFixed(2)},${y2.toFixed(2)} L${x3.toFixed(2)},${y3.toFixed(2)} A${R_IN},${R_IN} 0 ${lg},0 ${x4.toFixed(2)},${y4.toFixed(2)} Z\" fill=\"${col}\"/>`;\n}).join('');\n\nconst donutSvg = `<svg width=\"128\" height=\"128\" viewBox=\"0 0 128 128\" xmlns=\"http://www.w3.org/2000/svg\">\n  ${donutPaths}\n  <text x=\"64\" y=\"58\" text-anchor=\"middle\" font-size=\"22\" font-weight=\"700\" fill=\"${TEXT1}\" font-family=\"-apple-system,Arial,sans-serif\">${metrics.totalIssues}</text>\n  <text x=\"64\" y=\"75\" text-anchor=\"middle\" font-size=\"10\" fill=\"${TEXT2}\" font-family=\"-apple-system,Arial,sans-serif\">issues</text>\n</svg>`;\n\nconst statusLegend = statusOverview.map(s => {\n  const col = STATUS_COLOR_MAP[s.status]||'#8993A4';\n  const pct = Math.round(s.count/totalIssues*100);\n  return `<tr>\n    <td style=\"padding:5px 10px 5px 0;width:14px;\">\n      <div style=\"width:10px;height:10px;border-radius:2px;background:${col};\"></div>\n    </td>\n    <td style=\"padding:5px 0;font-size:12px;color:${TEXT1};\">${s.status}</td>\n    <td style=\"padding:5px 0;text-align:right;white-space:nowrap;\">\n      <span style=\"font-size:12px;font-weight:700;color:${TEXT1};\">${s.count}</span>\n      <span style=\"font-size:11px;color:${TEXT3};margin-left:3px;\">(${pct}%)</span>\n    </td>\n  </tr>`;\n}).join('');\n\n// \u2500\u2500 Sprint progress band \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst sprintBandFill = Math.max(0, Math.min(100, sprintPct));\nconst sprintBand = `\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">\n  <tr>\n    <td style=\"padding-bottom:7px;font-size:10px;font-weight:700;letter-spacing:0.07em;color:${TEXT3};text-transform:uppercase;\">Sprint Timeline</td>\n    <td style=\"padding-bottom:7px;text-align:right;font-size:10px;font-weight:700;color:${daysColor};\">${daysLabel}</td>\n  </tr>\n  <tr>\n    <td colspan=\"2\" style=\"padding:0;\">\n      <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:#DFE1E6;border-radius:4px;height:10px;\">\n        <tr>\n          <td width=\"${sprintBandFill}%\" style=\"background:${barColor};height:10px;border-radius:4px;font-size:0;line-height:0;\">&nbsp;</td>\n          <td style=\"height:10px;font-size:0;line-height:0;\">&nbsp;</td>\n        </tr>\n      </table>\n    </td>\n  </tr>\n  <tr>\n    <td style=\"padding-top:5px;font-size:10px;color:${TEXT3};\">${sprintStartFmt}</td>\n    <td style=\"padding-top:5px;text-align:right;font-size:10px;color:${TEXT3};\">${sprintEndFmt}</td>\n  </tr>\n</table>`;\n\n// \u2500\u2500 Unified metric card (both rows use same template) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// label top \u00b7 value big \u00b7 sub small \u2014 consistent structure = consistent height\nconst metricCard = (label, value, sub, borderCol, bgCol, valCol) =>\n  `<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"><tr>\n    <td style=\"border-left:3px solid ${borderCol};background:${bgCol};border-radius:0 5px 5px 0;padding:13px 14px;vertical-align:top;\">\n      <div style=\"font-size:9px;font-weight:700;color:${valCol};text-transform:uppercase;letter-spacing:0.08em;margin-bottom:6px;\">${label}</div>\n      <div style=\"font-size:22px;font-weight:700;color:${valCol};line-height:1;\">${value}</div>\n      <div style=\"font-size:10px;color:${TEXT3};margin-top:5px;line-height:1.35;\">${sub}</div>\n    </td>\n  </tr></table>`;\n\n// Aliases so rest of code stays unchanged\nconst kpi = (val, label, sub, border, bg, color) => metricCard(label, val, sub, border, bg, color);\nconst sprintKpi = metricCard;\n\n// \u2500\u2500 Total Issues prominent banner card \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Breakdown: done / in-progress / blocked counts from kpis\nconst tiDone      = kpis.doneCount || 0;\nconst tiInProg    = kpis.inProgressCount || 0;\nconst tiToDo      = kpis.toDoCount || 0;\nconst tiBlocked   = kpis.blockedCount || 0;\nconst tiOverdue   = kpis.overdueCount || 0;\n\nconst totalIssuesBanner = `\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin-bottom:12px;\">\n  <tr>\n    <td style=\"background:${BRAND};border-radius:6px;padding:14px 20px;\">\n      <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"><tr valign=\"middle\">\n\n        <!-- Big number -->\n        <td style=\"padding-right:20px;border-right:1px solid rgba(255,255,255,0.15);white-space:nowrap;width:1%;\">\n          <div style=\"font-size:9px;font-weight:700;letter-spacing:0.1em;color:rgba(255,255,255,0.4);text-transform:uppercase;margin-bottom:3px;\">Total Issues</div>\n          <div style=\"font-size:36px;font-weight:700;color:#FFFFFF;line-height:1;\">${metrics.totalIssues}</div>\n          <div style=\"font-size:9px;color:rgba(255,255,255,0.35);margin-top:3px;\">${sprintName}</div>\n        </td>\n\n        <!-- Breakdown pills \u2014 evenly spaced -->\n        <td style=\"padding-left:20px;\">\n          <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"><tr>\n\n            <td style=\"padding-right:8px;\">\n              <div style=\"background:rgba(54,179,126,0.18);border:1px solid rgba(54,179,126,0.3);border-radius:4px;padding:7px 10px;text-align:center;\">\n                <div style=\"font-size:16px;font-weight:700;color:#36B37E;line-height:1;\">${tiDone}</div>\n                <div style=\"font-size:8px;font-weight:700;color:rgba(255,255,255,0.45);text-transform:uppercase;letter-spacing:0.07em;margin-top:3px;\">Done</div>\n              </div>\n            </td>\n\n            <td style=\"padding-right:8px;\">\n              <div style=\"background:rgba(0,82,204,0.2);border:1px solid rgba(0,82,204,0.3);border-radius:4px;padding:7px 10px;text-align:center;\">\n                <div style=\"font-size:16px;font-weight:700;color:#579DFF;line-height:1;\">${tiInProg}</div>\n                <div style=\"font-size:8px;font-weight:700;color:rgba(255,255,255,0.45);text-transform:uppercase;letter-spacing:0.07em;margin-top:3px;\">Active</div>\n              </div>\n            </td>\n\n            <td style=\"padding-right:8px;\">\n              <div style=\"background:rgba(255,255,255,0.07);border:1px solid rgba(255,255,255,0.13);border-radius:4px;padding:7px 10px;text-align:center;\">\n                <div style=\"font-size:16px;font-weight:700;color:rgba(255,255,255,0.6);line-height:1;\">${tiToDo}</div>\n                <div style=\"font-size:8px;font-weight:700;color:rgba(255,255,255,0.45);text-transform:uppercase;letter-spacing:0.07em;margin-top:3px;\">To Do</div>\n              </div>\n            </td>\n\n            ${tiBlocked > 0 ? `\n            <td style=\"padding-right:8px;\">\n              <div style=\"background:rgba(191,38,0,0.18);border:1px solid rgba(191,38,0,0.35);border-radius:4px;padding:7px 10px;text-align:center;\">\n                <div style=\"font-size:16px;font-weight:700;color:#FF7452;line-height:1;\">${tiBlocked}</div>\n                <div style=\"font-size:8px;font-weight:700;color:rgba(255,255,255,0.45);text-transform:uppercase;letter-spacing:0.07em;margin-top:3px;\">Blocked</div>\n              </div>\n            </td>` : ''}\n\n            ${tiOverdue > 0 ? `\n            <td>\n              <div style=\"background:rgba(255,139,0,0.15);border:1px solid rgba(255,139,0,0.35);border-radius:4px;padding:7px 10px;text-align:center;\">\n                <div style=\"font-size:16px;font-weight:700;color:#FF8B00;line-height:1;\">${tiOverdue}</div>\n                <div style=\"font-size:8px;font-weight:700;color:rgba(255,255,255,0.45);text-transform:uppercase;letter-spacing:0.07em;margin-top:3px;\">Overdue</div>\n              </div>\n            </td>` : ''}\n\n          </tr></table>\n        </td>\n\n      </tr></table>\n    </td>\n  </tr>\n</table>`;\n\n// \u2500\u2500 Priority bars \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst maxP = Math.max(...priorityBreakdown.map(p=>p.count),1);\nconst priorityRows = priorityBreakdown.map(p => {\n  const w = Math.round((p.count/maxP)*100);\n  const col = priorityColor(p.priority);\n  return `<tr>\n    <td style=\"padding:7px 14px 7px 0;white-space:nowrap;width:1%;\">\n      ${pDot(p.priority)}<span style=\"font-size:12px;color:${TEXT1};\">${p.priority}</span>\n    </td>\n    <td style=\"padding:7px 0;\">\n      <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:#DFE1E6;border-radius:2px;height:10px;\">\n        <tr>\n          <td width=\"${w}%\" style=\"background:${col};height:10px;border-radius:2px;font-size:0;line-height:0;\">&nbsp;</td>\n          <td style=\"height:10px;font-size:0;line-height:0;\">&nbsp;</td>\n        </tr>\n      </table>\n    </td>\n    <td style=\"padding:7px 0 7px 12px;text-align:right;white-space:nowrap;width:1%;font-size:12px;font-weight:700;color:${TEXT2};\">${p.count}</td>\n  </tr>`;\n}).join('');\n\n// \u2500\u2500 Team workload \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst TEAM_COLORS = ['#0052CC','#6554C0','#36B37E','#FF8B00','#BF2600'];\nconst workloadRows = teamWorkload.map((m,idx) => {\n  const initials = m.assignee.split(' ').map(w=>w[0]).join('').substring(0,2).toUpperCase();\n  const col = TEAM_COLORS[idx%TEAM_COLORS.length];\n  return `<tr>\n    <td style=\"padding:8px 14px 8px 0;white-space:nowrap;width:1%;\">\n      <table cellpadding=\"0\" cellspacing=\"0\" style=\"border-collapse:separate;\">\n        <tr>\n          <td width=\"32\" height=\"32\" style=\"width:32px;height:32px;min-width:32px;background:${col};border-radius:16px;text-align:center;vertical-align:middle;font-size:11px;font-weight:700;color:#FFFFFF;line-height:32px;padding:0;\">${initials}</td>\n          <td style=\"padding-left:10px;vertical-align:middle;white-space:nowrap;font-size:13px;font-weight:500;color:${TEXT1};\">${m.assignee}</td>\n        </tr>\n      </table>\n    </td>\n    <td style=\"padding:8px 0;\">\n      <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:#DFE1E6;border-radius:2px;height:10px;\">\n        <tr>\n          <td width=\"${m.pct}%\" style=\"background:${col};height:10px;border-radius:2px;font-size:0;line-height:0;\">&nbsp;</td>\n          <td style=\"height:10px;font-size:0;line-height:0;\">&nbsp;</td>\n        </tr>\n      </table>\n    </td>\n    <td style=\"padding:8px 0 8px 12px;text-align:right;white-space:nowrap;width:1%;\">\n      <span style=\"font-size:12px;font-weight:700;color:${TEXT2};\">${m.count}</span>\n      <span style=\"font-size:11px;color:${TEXT3};margin-left:3px;\">(${m.pct}%)</span>\n    </td>\n  </tr>`;\n}).join('');\n\n// \u2500\u2500 Epic rows \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst epicRows = epicProgress.length===0\n  ? `<tr><td style=\"padding:16px 0;font-size:12px;color:${TEXT3};text-align:center;\">No themes (epics) found in this sprint.</td></tr>`\n  : epicProgress.map(e => `\n  <tr>\n    <td style=\"padding:12px 0;border-bottom:1px solid ${BORDER};\">\n      <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"><tr valign=\"middle\">\n        <td>\n          <div style=\"margin-bottom:7px;\">\n            <a href=\"${JIRA_BASE}/${e.key}\" style=\"font-size:11px;font-weight:700;color:${BRAND_ACC};text-decoration:none;\">${e.key}</a>\n            <span style=\"font-size:13px;font-weight:600;color:${TEXT1};margin-left:8px;\">${e.name.substring(0,55)}</span>\n          </div>\n          <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:#DFE1E6;border-radius:3px;height:8px;overflow:hidden;\">\n            <tr>\n              ${e.donePct>0 ? `<td width=\"${e.donePct}%\" style=\"background:#36B37E;height:8px;font-size:0;line-height:0;\">&nbsp;</td>` : \"\"}\n              ${e.inProgPct>0 ? `<td width=\"${e.inProgPct}%\" style=\"background:#0052CC;height:8px;font-size:0;line-height:0;\">&nbsp;</td>` : \"\"}\n              <td style=\"height:8px;font-size:0;line-height:0;\">&nbsp;</td>\n            </tr>\n          </table>\n        </td>\n        <td style=\"width:130px;padding-left:16px;text-align:right;white-space:nowrap;vertical-align:middle;\">\n          <span style=\"font-size:12px;font-weight:700;color:#006644;\">${e.donePct}%</span>\n          <span style=\"font-size:11px;color:${TEXT3};\"> done</span><br>\n          <span style=\"font-size:11px;color:#0052CC;\">${e.inProgPct}% active</span>\n        </td>\n      </tr></table>\n    </td>\n  </tr>`).join('');\n\n// \u2500\u2500 Activity rows \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst activityRows = recentActivity.length===0\n  ? `<tr><td style=\"padding:16px 0;font-size:12px;color:${TEXT3};\">No activity this week.</td></tr>`\n  : recentActivity.map(a => `\n  <tr style=\"border-bottom:1px solid ${BORDER};\">\n    <td style=\"padding:11px 12px 11px 0;vertical-align:top;width:1%;white-space:nowrap;\">\n      <a href=\"${JIRA_BASE}/${a.key}\" style=\"font-size:11px;font-weight:700;color:${BRAND_ACC};text-decoration:none;\">${a.key}</a>\n    </td>\n    <td style=\"padding:11px 12px 11px 0;vertical-align:top;\">\n      <div style=\"font-size:13px;color:${TEXT1};font-weight:500;line-height:1.4;margin-bottom:3px;\">${a.summary}</div>\n      <div style=\"font-size:11px;color:${TEXT3};\">${a.type||'Issue'}&nbsp;\u00b7&nbsp;${a.assignee}&nbsp;\u00b7&nbsp;${a.updated}</div>\n    </td>\n    <td style=\"padding:11px 0;vertical-align:top;white-space:nowrap;text-align:right;width:1%;\">\n      <span style=\"background:${statusBg(a.status)};color:${statusColor(a.status)};border-radius:3px;padding:3px 8px;font-size:11px;font-weight:700;\">${a.status}</span>\n      <div style=\"margin-top:5px;\">${pDot(a.priority)}<span style=\"font-size:11px;color:${TEXT3};\">${a.priority}</span></div>\n    </td>\n  </tr>`).join('');\n\n// \u2500\u2500 Due soon rows \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst dueSoonRows = dueSoonIssues.length===0\n  ? `<tr><td colspan=\"5\" style=\"padding:14px 0;font-size:12px;color:${TEXT3};\">No deadlines in the next 7 days.</td></tr>`\n  : dueSoonIssues.map(i => `\n  <tr style=\"border-bottom:1px solid ${BORDER};\">\n    <td style=\"padding:10px 12px 10px 0;white-space:nowrap;\">\n      <a href=\"${JIRA_BASE}/${i.key}\" style=\"font-size:11px;font-weight:700;color:${BRAND_ACC};text-decoration:none;\">${i.key}</a>\n    </td>\n    <td style=\"padding:10px 12px;font-size:12px;color:${TEXT1};\">${i.summary}</td>\n    <td style=\"padding:10px 12px;white-space:nowrap;\">${pDot(i.priority)}<span style=\"font-size:12px;color:${TEXT2};\">${i.priority||'\u2014'}</span></td>\n    <td style=\"padding:10px 12px;font-size:12px;font-weight:700;color:#BF2600;white-space:nowrap;\">${i.duedate||'\u2014'}</td>\n    <td style=\"padding:10px 0;font-size:12px;color:${TEXT2};\">${i.assignee}</td>\n  </tr>`).join('');\n\n// \u2500\u2500 Section header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst sh = (label, sub='') =>\n  `<div style=\"border-bottom:1px solid ${BORDER};padding-bottom:10px;margin-bottom:18px;\">\n    <span style=\"font-size:14px;font-weight:700;color:${TEXT1};\">${label}</span>\n    ${sub?`<span style=\"font-size:11px;color:${TEXT3};margin-left:8px;\">${sub}</span>`:''}\n  </div>`;\n\n// \u2500\u2500 KPI section HTML \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst healthColor = {\n  'On Track':    '#006644',\n  'Slight Risk': '#FF8B00',\n  'At Risk':     '#BF2600',\n  'Critical':    '#BF2600'\n}[kpis.sprintHealth] || TEXT2;\n\nconst healthBg = {\n  'On Track':    '#E3FCEF',\n  'Slight Risk': '#FFFAE6',\n  'At Risk':     '#FFEBE6',\n  'Critical':    '#FFEBE6'\n}[kpis.sprintHealth] || SURFACE2;\n\n// Assignee KPI table rows\nconst assigneeKPIRows = kpis.assigneeKPIs.map((a, idx) => {\n  const col = TEAM_COLORS[idx % TEAM_COLORS.length];\n  const initials = a.assignee.split(' ').map(w=>w[0]).join('').substring(0,2).toUpperCase();\n  const ltDisplay = a.avgLeadTime !== null ? `${a.avgLeadTime}d` : '\u2014';\n  return `<tr style=\"border-bottom:1px solid ${BORDER};\">\n    <td style=\"padding:10px 12px 10px 0;white-space:nowrap;\">\n      <table cellpadding=\"0\" cellspacing=\"0\" style=\"border-collapse:separate;\"><tr>\n        <td width=\"28\" height=\"28\" style=\"width:28px;height:28px;min-width:28px;background:${col};border-radius:14px;text-align:center;vertical-align:middle;font-size:10px;font-weight:700;color:#FFFFFF;line-height:28px;padding:0;\">${initials}</td>\n        <td style=\"padding-left:8px;font-size:12px;font-weight:500;color:${TEXT1};white-space:nowrap;vertical-align:middle;\">${a.assignee}</td>\n      </tr></table>\n    </td>\n    <td style=\"padding:10px 10px;text-align:center;font-size:12px;font-weight:700;color:#006644;\">${a.done}</td>\n    <td style=\"padding:10px 10px;text-align:center;font-size:12px;color:#0052CC;\">${a.inProgress}</td>\n    <td style=\"padding:10px 10px;text-align:center;font-size:12px;color:${TEXT3};\">${a.toDo}</td>\n    <td style=\"padding:10px 10px;text-align:center;font-size:12px;font-weight:700;color:${a.blocked>0?'#BF2600':TEXT3};\">${a.blocked>0?a.blocked:'\u2014'}</td>\n    <td style=\"padding:10px 10px;text-align:center;font-size:12px;color:${TEXT2};\">${a.spDone > 0 ? a.spDone : '\u2014'}</td>\n    <td style=\"padding:10px 10px;text-align:center;font-size:12px;font-weight:700;color:#0052CC;\">${a.spDoneWeek > 0 ? a.spDoneWeek : '\u2014'}</td>\n    <td style=\"padding:10px 0;text-align:right;font-size:12px;color:${TEXT2};\">${ltDisplay}</td>\n  </tr>`;\n}).join('');\n\n// Lead time by priority rows\nconst ltPrioRows = kpis.leadTimeByPriority.length > 0\n  ? kpis.leadTimeByPriority.map(lt =>\n    `<tr style=\"border-bottom:1px solid ${BORDER};\">\n      <td style=\"padding:8px 12px 8px 0;font-size:12px;color:${TEXT1};\">${pDot(lt.priority)}${lt.priority}</td>\n      <td style=\"padding:8px 10px;text-align:right;font-size:13px;font-weight:700;color:${TEXT1};\">${lt.avgDays}d</td>\n      <td style=\"padding:8px 0;text-align:right;font-size:11px;color:${TEXT3};\">${lt.count} issue${lt.count!==1?'s':''}</td>\n    </tr>`\n  ).join('')\n  : `<tr><td colspan=\"3\" style=\"padding:12px 0;font-size:12px;color:${TEXT3};text-align:center;\">No resolved issues yet</td></tr>`;\n\n// Blocked list\nconst blockedHTML = kpis.blockedList.length === 0\n  ? `<div style=\"font-size:12px;color:${TEXT3};padding:12px 0;\">No blocked issues \u2705</div>`\n  : kpis.blockedList.map(b =>\n    `<div style=\"padding:8px 0;border-bottom:1px solid ${BORDER};\">\n      <a href=\"${JIRA_BASE}/${b.key}\" style=\"font-size:11px;font-weight:700;color:${BRAND_ACC};text-decoration:none;\">${b.key}</a>\n      <span style=\"font-size:12px;color:${TEXT1};margin-left:6px;\">${b.summary}</span>\n      <div style=\"margin-top:3px;font-size:11px;color:${TEXT3};\">\n        ${b.assignee}${b.daysBlocked!==null?` \u00b7 Blocked for <strong style=\"color:#BF2600;\">${b.daysBlocked}d</strong>`:''}\n      </div>\n    </div>`\n  ).join('');\n\n// Overdue list\nconst overdueHTML = kpis.overdueList.length === 0\n  ? `<div style=\"font-size:12px;color:${TEXT3};padding:12px 0;\">No overdue issues \u2705</div>`\n  : kpis.overdueList.map(o =>\n    `<div style=\"padding:8px 0;border-bottom:1px solid ${BORDER};\">\n      <a href=\"${JIRA_BASE}/${o.key}\" style=\"font-size:11px;font-weight:700;color:${BRAND_ACC};text-decoration:none;\">${o.key}</a>\n      <span style=\"font-size:12px;color:${TEXT1};margin-left:6px;\">${o.summary}</span>\n      <div style=\"margin-top:3px;font-size:11px;color:${TEXT3};\">\n        Due: <strong style=\"color:#BF2600;\">${o.duedate}</strong> \u00b7 ${o.daysOverdue}d overdue \u00b7 ${o.assignee} \u00b7\n        <span style=\"background:${statusBg(o.status)};color:${statusColor(o.status)};border-radius:3px;padding:1px 6px;font-size:10px;font-weight:700;\">${o.status}</span>\n      </div>\n    </div>`\n  ).join('');\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// DERIVED SUMMARY VALUES\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n// Epic completion summary sentence\nconst epicsDone   = epicProgress.filter(e => e.donePct === 100).length;\nconst epicsTotal  = epicProgress.length;\nconst epicSummary = epicsTotal > 0\n  ? `${epicsDone} of ${epicsTotal} theme${epicsTotal!==1?'s':''} fully completed.`\n  : 'No themes tracked in this sprint.';\n\n// Executive summary sentence\nconst hasRisks = kpis.blockedCount > 0 || kpis.overdueCount > 0;\nconst execSummaryText = (() => {\n  const parts = [];\n  parts.push(`Sprint is <strong>${sprintDaysLeft===0?'complete':sprintPct+'% elapsed'}</strong> with <strong>${kpis.completionRate}% of work delivered</strong> (${kpis.doneCount} of ${metrics.totalIssues} issues done).`);\n  if (kpis.blockedCount > 0) parts.push(`<strong style=\"color:#BF2600;\">${kpis.blockedCount} issue${kpis.blockedCount!==1?'s are':' is'} blocked</strong> and require${kpis.blockedCount===1?'s':''} immediate attention.`);\n  if (kpis.overdueCount > 0) parts.push(`<strong style=\"color:#BF2600;\">${kpis.overdueCount} item${kpis.overdueCount!==1?'s are':' is'} overdue</strong>.`);\n  if (!hasRisks) parts.push('No blockers or overdue items. \ud83d\udfe2');\n  return parts.join(' ');\n})();\n\n// Alert banner \u2014 only shown when there are risks\nconst alertBanner = hasRisks ? `\n<tr><td style=\"background:#FFF3CD;border-left:4px solid #FF8B00;border-right:1px solid ${BORDER};padding:12px 20px;\">\n  <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"><tr valign=\"middle\">\n    <td style=\"font-size:11px;font-weight:700;color:#7A4F00;text-transform:uppercase;letter-spacing:0.07em;width:1%;white-space:nowrap;padding-right:14px;\">\u26a0 Action Required</td>\n    <td style=\"font-size:12px;color:#5C3D00;line-height:1.5;\">\n      ${kpis.blockedList.map(b=>`<strong>${b.key}</strong> blocked for ${b.daysBlocked}d \u2014 ${b.assignee}`).join(' &nbsp;\u00b7&nbsp; ')}\n      ${kpis.overdueList.length > 0 ? ' &nbsp;\u00b7&nbsp; ' + kpis.overdueList.map(o=>`<strong>${o.key}</strong> overdue ${o.daysOverdue}d`).join(', ') : ''}\n    </td>\n  </tr></table>\n</td></tr>` : '';\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// ASSEMBLE \u2014 Business-reader narrative order:\n// 1. Header\n// 2. Alert banner (risks only)\n// 3. Executive summary\n// 4. Sprint Timeline (context for everything below)\n// 5. Metrics strip (totals + sprint KPIs + weekly activity)\n// 6. Sprint Status (donut) + Theme Progress side by side\n// 7. Risks: Blocked & Overdue (prominent)\n// 8. Team Workload + Priority Distribution\n// 9. Recent Activity\n// 10. Detailed Assignee breakdown + Delivery Time\n// 11. Upcoming Deadlines\n// 12. Footer\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nconst html = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<title>${reportTitle}</title>\n</head>\n<body style=\"margin:0;padding:0;background:#E4E8EF;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Helvetica Neue',Arial,sans-serif;\">\n\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:#E4E8EF;padding:28px 0;\">\n<tr><td align=\"center\">\n<table width=\"620\" cellpadding=\"0\" cellspacing=\"0\" style=\"max-width:620px;width:100%;\">\n\n<!-- \u2550\u2550 1. HEADER \u2550\u2550 -->\n<tr><td style=\"background:${BRAND};border-radius:8px 8px 0 0;padding:26px 30px;\">\n  <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"><tr valign=\"top\">\n    <td style=\"padding-right:16px;\">\n      <div style=\"font-size:9px;font-weight:700;letter-spacing:0.1em;color:rgba(255,255,255,0.4);text-transform:uppercase;margin-bottom:8px;\">${projectKey} &nbsp;/&nbsp; Weekly Status Report</div>\n      <div style=\"font-size:19px;font-weight:700;color:#FFFFFF;line-height:1.3;margin-bottom:5px;\">${projectShort}</div>\n      <div style=\"font-size:11px;color:rgba(255,255,255,0.5);\">${dateRange}</div>\n      ${sprintGoal?`<div style=\"font-size:11px;color:rgba(255,255,255,0.45);margin-top:8px;border-left:2px solid rgba(255,255,255,0.2);padding-left:8px;font-style:italic;\">Goal: ${sprintGoal}</div>`:''}\n    </td>\n    <td style=\"width:170px;vertical-align:top;\">\n      <table width=\"170\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:rgba(255,255,255,0.11);border:1px solid rgba(255,255,255,0.15);border-radius:6px;\">\n      <tr><td style=\"padding:12px 14px;\">\n        <div style=\"font-size:9px;font-weight:700;letter-spacing:0.09em;color:rgba(255,255,255,0.4);text-transform:uppercase;margin-bottom:5px;\">Active Sprint</div>\n        <div style=\"font-size:13px;font-weight:700;color:#FFFFFF;margin-bottom:2px;\">${sprintName}</div>\n        <div style=\"font-size:10px;color:rgba(255,255,255,0.5);margin-bottom:10px;\">${sprintDates}</div>\n        <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"><tr>\n          <td style=\"font-size:9px;color:rgba(255,255,255,0.4);\">${sprintPct}% elapsed</td>\n          <td style=\"text-align:right;font-size:9px;font-weight:700;color:${sprintDaysLeft===0?'#FF8B00':'rgba(255,255,255,0.6)'};\">${daysLabel}</td>\n        </tr></table>\n      </td></tr>\n      </table>\n    </td>\n  </tr></table>\n</td></tr>\n\n<!-- \u2550\u2550 2. ALERT BANNER (risks only) \u2550\u2550 -->\n${alertBanner}\n\n<!-- \u2550\u2550 3. EXECUTIVE SUMMARY \u2550\u2550 -->\n<tr><td style=\"background:${SURFACE2};border-left:1px solid ${BORDER};border-right:1px solid ${BORDER};border-top:1px solid ${BORDER};padding:14px 24px;\">\n  <div style=\"font-size:12px;color:${TEXT1};line-height:1.7;\">${execSummaryText}</div>\n</td></tr>\n\n<!-- \u2550\u2550 4. SPRINT TIMELINE \u2550\u2550 -->\n<tr><td style=\"background:#F0F3F8;border:1px solid ${BORDER};padding:14px 30px;\">\n  ${sprintBand}\n</td></tr>\n\n<!-- \u2550\u2550 5. UNIFIED METRICS STRIP \u2550\u2550 -->\n<tr><td style=\"background:${SURFACE};border-left:1px solid ${BORDER};border-right:1px solid ${BORDER};padding:20px 30px;\">\n\n  ${totalIssuesBanner}\n\n  <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin-bottom:8px;table-layout:fixed;\"><tr valign=\"top\">\n    <td style=\"padding-right:6px;\">${sprintKpi('Sprint Health', kpis.sprintHealth, kpis.healthGap > 0 ? kpis.healthGap + '% behind target' : 'Ahead of schedule', healthColor, healthBg, healthColor)}</td>\n    <td style=\"padding:0 6px;\">${sprintKpi('Velocity', kpis.storyPointsWeek + ' SP / week', kpis.storyPointsCompleted + ' SP done \u00b7 ' + kpis.storyPointsTotal + ' SP scope', '#0052CC', '#EEF4FF', '#0052CC')}</td>\n    <td style=\"padding:0 6px;\">${sprintKpi('Avg Delivery Time (Lead Time)', kpis.avgLeadTime !== null ? kpis.avgLeadTime + 'd' : '\u2014', kpis.avgLeadTime !== null ? kpis.minLeadTime + 'd \u2013 ' + kpis.maxLeadTime + 'd range' : 'No resolved issues', '#6554C0', '#F3F0FF', '#6554C0')}</td>\n    <td style=\"padding-left:6px;\">${sprintKpi('Completion', kpis.completionRate + '%', kpis.doneCount + ' of ' + (kpis.doneCount + kpis.inProgressCount + kpis.toDoCount) + ' issues done', '#36B37E', '#F2FBF6', '#006644')}</td>\n  </tr></table>\n\n  <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin:10px 0;\"><tr>\n    <td style=\"border-top:1px dashed ${BORDER};font-size:0;line-height:0;\">&nbsp;</td>\n  </tr></table>\n\n  <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"table-layout:fixed;\"><tr valign=\"top\">\n    <td style=\"padding-right:6px;\">${kpi(metrics.completed, 'Completed', 'this week', '#36B37E', '#F2FBF6', '#006644')}</td>\n    <td style=\"padding:0 6px;\">${kpi(metrics.updated, 'Updated', 'this week', '#0052CC', '#EEF4FF', '#0052CC')}</td>\n    <td style=\"padding:0 6px;\">${kpi(metrics.created, 'Created', 'this week', '#6554C0', '#F3F0FF', '#6554C0')}</td>\n    <td style=\"padding-left:6px;\">${kpi(metrics.dueSoon, 'Due Soon', 'next 7 days', '#FF8B00', '#FFFAE6', '#FF8B00')}</td>\n  </tr></table>\n\n</td></tr>\n\n<!-- \u2550\u2550 6. SPRINT STATUS + THEME PROGRESS side by side \u2550\u2550 -->\n<tr><td style=\"background:${SURFACE};border-left:1px solid ${BORDER};border-right:1px solid ${BORDER};border-top:1px solid ${BORDER};padding:24px 30px;\">\n  <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"><tr valign=\"top\">\n\n    <!-- Sprint Status donut -->\n    <td width=\"34%\" style=\"padding-right:22px;border-right:1px solid ${BORDER};\">\n      ${sh('Sprint Status')}\n      <div style=\"text-align:center;margin-bottom:14px;\">${donutSvg}</div>\n      <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">${statusLegend}</table>\n    </td>\n\n    <!-- Theme Progress -->\n    <td style=\"padding-left:22px;\">\n      <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"><tr valign=\"middle\">\n        <td>${sh('Theme Progress (Epic)')}</td>\n        <td style=\"text-align:right;vertical-align:top;padding-top:2px;white-space:nowrap;\">\n          <span style=\"display:inline-block;width:8px;height:8px;border-radius:2px;background:#36B37E;margin-right:3px;vertical-align:middle;\"></span><span style=\"font-size:10px;color:${TEXT3};margin-right:8px;\">Done</span>\n          <span style=\"display:inline-block;width:8px;height:8px;border-radius:2px;background:#0052CC;margin-right:3px;vertical-align:middle;\"></span><span style=\"font-size:10px;color:${TEXT3};margin-right:8px;\">Active</span>\n          <span style=\"display:inline-block;width:8px;height:8px;border-radius:2px;background:#DFE1E6;margin-right:3px;vertical-align:middle;\"></span><span style=\"font-size:10px;color:${TEXT3};\">To Do</span>\n        </td>\n      </tr></table>\n      <div style=\"font-size:11px;color:${TEXT2};margin-bottom:12px;margin-top:-6px;\">${epicSummary}</div>\n      <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">${epicRows}</table>\n    </td>\n\n  </tr></table>\n</td></tr>\n\n<!-- \u2550\u2550 7. RISKS: BLOCKED & OVERDUE \u2550\u2550 -->\n<tr><td style=\"background:${SURFACE};border-left:1px solid ${BORDER};border-right:1px solid ${BORDER};border-top:1px solid ${BORDER};padding:24px 30px;\">\n  <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"><tr valign=\"top\">\n\n    <td width=\"50%\" style=\"padding-right:20px;border-right:1px solid ${BORDER};\">\n      ${sh('Blocked Items', kpis.blockedCount > 0 ? String(kpis.blockedCount) : '\u2705 None')}\n      ${blockedHTML}\n    </td>\n\n    <td style=\"padding-left:20px;\">\n      ${sh('Overdue Items', kpis.overdueCount > 0 ? String(kpis.overdueCount) : '\u2705 None')}\n      ${overdueHTML}\n    </td>\n\n  </tr></table>\n</td></tr>\n\n<!-- \u2550\u2550 8. TEAM WORKLOAD + PRIORITY DISTRIBUTION \u2550\u2550 -->\n<tr><td style=\"background:${SURFACE};border-left:1px solid ${BORDER};border-right:1px solid ${BORDER};border-top:1px solid ${BORDER};padding:24px 30px;\">\n  <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"><tr valign=\"top\">\n    <td width=\"50%\" style=\"padding-right:20px;border-right:1px solid ${BORDER};\">\n      ${sh('Team Workload')}\n      <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">${workloadRows}</table>\n    </td>\n    <td style=\"padding-left:20px;\">\n      ${sh('Priority Distribution')}\n      <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">${priorityRows}</table>\n    </td>\n  </tr></table>\n</td></tr>\n\n<!-- \u2550\u2550 9. RECENT ACTIVITY \u2550\u2550 -->\n<tr><td style=\"background:${SURFACE};border-left:1px solid ${BORDER};border-right:1px solid ${BORDER};border-top:1px solid ${BORDER};padding:24px 30px;\">\n  ${sh('Recent Activity', 'Changes made this week')}\n  <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">${activityRows}</table>\n</td></tr>\n\n<!-- \u2550\u2550 10. DETAILED BREAKDOWN: PER-ASSIGNEE + DELIVERY TIME \u2550\u2550 -->\n<tr><td style=\"background:${SURFACE};border-left:1px solid ${BORDER};border-right:1px solid ${BORDER};border-top:1px solid ${BORDER};padding:24px 30px;\">\n\n  ${sh('Per-Person Breakdown (Assignee)')}\n  <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">\n    <tr style=\"background:${SURFACE2};\">\n      <th style=\"padding:7px 12px 7px 0;font-size:10px;font-weight:700;color:${TEXT3};text-transform:uppercase;text-align:left;letter-spacing:0.05em;\">Person</th>\n      <th style=\"padding:7px 10px;font-size:10px;font-weight:700;color:#006644;text-transform:uppercase;text-align:center;letter-spacing:0.05em;\">Done</th>\n      <th style=\"padding:7px 10px;font-size:10px;font-weight:700;color:#0052CC;text-transform:uppercase;text-align:center;letter-spacing:0.05em;\">Active</th>\n      <th style=\"padding:7px 10px;font-size:10px;font-weight:700;color:${TEXT3};text-transform:uppercase;text-align:center;letter-spacing:0.05em;\">To Do</th>\n      <th style=\"padding:7px 10px;font-size:10px;font-weight:700;color:#BF2600;text-transform:uppercase;text-align:center;letter-spacing:0.05em;\">Blocked</th>\n      <th style=\"padding:7px 10px;font-size:10px;font-weight:700;color:${TEXT3};text-transform:uppercase;text-align:center;letter-spacing:0.05em;\">SP Done</th>\n      <th style=\"padding:7px 10px;font-size:10px;font-weight:700;color:#0052CC;text-transform:uppercase;text-align:center;letter-spacing:0.05em;\">SP Week</th>\n      <th style=\"padding:7px 0;font-size:10px;font-weight:700;color:${TEXT3};text-transform:uppercase;text-align:right;letter-spacing:0.05em;\">Avg Delivery</th>\n    </tr>\n    ${assigneeKPIRows}\n  </table>\n\n  <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin-top:24px;\"><tr valign=\"top\">\n    <td style=\"padding-right:20px;\">\n      ${sh('Delivery Time by Priority (Lead Time)')}\n      <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">${ltPrioRows}</table>\n    </td>\n  </tr></table>\n\n</td></tr>\n\n<!-- \u2550\u2550 11. UPCOMING DEADLINES \u2550\u2550 -->\n${dueSoonIssues.length>0?`\n<tr><td style=\"background:${SURFACE};border-left:1px solid ${BORDER};border-right:1px solid ${BORDER};border-top:1px solid ${BORDER};padding:24px 30px;\">\n  ${sh('Upcoming Deadlines', 'Next 7 days')}\n  <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">\n    <tr style=\"background:${SURFACE2};\">\n      <th style=\"padding:7px 10px 7px 0;font-size:10px;font-weight:700;color:${TEXT3};text-transform:uppercase;text-align:left;letter-spacing:0.05em;\">Issue</th>\n      <th style=\"padding:7px 10px;font-size:10px;font-weight:700;color:${TEXT3};text-transform:uppercase;text-align:left;letter-spacing:0.05em;\">Summary</th>\n      <th style=\"padding:7px 10px;font-size:10px;font-weight:700;color:${TEXT3};text-transform:uppercase;text-align:left;letter-spacing:0.05em;\">Priority</th>\n      <th style=\"padding:7px 10px;font-size:10px;font-weight:700;color:${TEXT3};text-transform:uppercase;text-align:left;letter-spacing:0.05em;\">Due Date</th>\n      <th style=\"padding:7px 0;font-size:10px;font-weight:700;color:${TEXT3};text-transform:uppercase;text-align:left;letter-spacing:0.05em;\">Assignee</th>\n    </tr>\n    ${dueSoonRows}\n  </table>\n</td></tr>`:''}\n\n<!-- \u2550\u2550 BOTTOM PADDING \u2550\u2550 -->\n<tr><td style=\"background:${SURFACE};border-left:1px solid ${BORDER};border-right:1px solid ${BORDER};padding-bottom:20px;\"></td></tr>\n\n<!-- \u2550\u2550 12. FOOTER \u2550\u2550 -->\n<tr><td style=\"background:${BRAND};border-radius:0 0 8px 8px;padding:18px 30px;\">\n  <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"><tr valign=\"top\">\n    <td>\n      <div style=\"font-size:12px;color:rgba(255,255,255,0.85);line-height:1.5;\">\n        Auto-generated by the <strong style=\"color:#FFFFFF;\">n8n Reporting Agent</strong> for <strong style=\"color:#FFFFFF;\">${projectName}</strong>\n      </div>\n      <div style=\"font-size:10px;color:rgba(255,255,255,0.38);margin-top:5px;\">\n        ${sprintName}&nbsp;\u00b7&nbsp;${sprintDates}&nbsp;\u00b7&nbsp;Jira \u2014 IG Green Team\n      </div>\n    </td>\n    <td style=\"text-align:right;padding-left:16px;white-space:nowrap;\">\n      <div style=\"font-size:10px;color:rgba(255,255,255,0.3);line-height:1.7;\">\n        Do not reply&nbsp;\u00b7&nbsp;Sent Mondays 08:00\n      </div>\n    </td>\n  </tr></table>\n</td></tr>\n\n</table>\n</td></tr>\n</table>\n</body>\n</html>`;\n\nreturn [{ json: {\n  fullHtml: html,\n  reportTitle\n} }];"
      },
      "typeVersion": 2
    },
    {
      "id": "34e8d77b-0bb7-47f7-af39-1644dfd824d1",
      "name": "Build Markdown Report",
      "type": "n8n-nodes-base.code",
      "position": [
        2832,
        6608
      ],
      "parameters": {
        "jsCode": "// ============================================================\n// BUILD MARKDOWN REPORT\n// Mirrors HTML report exactly \u2014 same section order, same data\n// ============================================================\n\nconst d = $input.first().json;\n\n// \u2500\u2500 Pull every field with safe fallbacks \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst projectKey    = d.projectKey   || 'PROJECT';\nconst projectName   = d.projectName  || projectKey;\nconst projectShort  = d.projectShort || projectName;\nconst sprintName    = d.sprintName   || 'Current Sprint';\nconst sprintGoal    = d.sprintGoal   || '';\nconst sprintDates   = d.sprintDates  || '';\nconst sprintStartFmt = d.sprintStartFmt || '';\nconst sprintEndFmt   = d.sprintEndFmt   || '';\nconst sprintPct     = d.sprintPct    ?? 0;\nconst sprintDaysLeft = d.sprintDaysLeft ?? null;\nconst reportTitle   = d.reportTitle  || 'Weekly Report';\nconst dateRange     = d.dateRange    || '';\n\nconst metrics = {\n  completed:   d.metrics?.completed   ?? 0,\n  updated:     d.metrics?.updated     ?? 0,\n  created:     d.metrics?.created     ?? 0,\n  dueSoon:     d.metrics?.dueSoon     ?? 0,\n  totalIssues: d.metrics?.totalIssues ?? 0,\n};\n\nconst k = d.kpis || {};\nconst kpis = {\n  storyPointsCompleted:  k.storyPointsCompleted  ?? 0,\n  storyPointsWeek:       k.storyPointsWeek       ?? 0,\n  storyPointsTotal:      k.storyPointsTotal      ?? 0,\n  storyPointsRemaining:  k.storyPointsRemaining  ?? 0,\n  completionRate:        k.completionRate        ?? 0,\n  throughputWeek:        k.throughputWeek        ?? 0,\n  throughputSprint:      k.throughputSprint      ?? 0,\n  sprintHealth:          k.sprintHealth          || 'N/A',\n  healthGap:             k.healthGap             ?? 0,\n  avgLeadTime:           k.avgLeadTime           ?? null,\n  minLeadTime:           k.minLeadTime           ?? null,\n  maxLeadTime:           k.maxLeadTime           ?? null,\n  doneCount:             k.doneCount             ?? 0,\n  inProgressCount:       k.inProgressCount       ?? 0,\n  toDoCount:             k.toDoCount             ?? 0,\n  blockedCount:          k.blockedCount          ?? 0,\n  overdueCount:          k.overdueCount          ?? 0,\n  leadTimeByPriority:    Array.isArray(k.leadTimeByPriority) ? k.leadTimeByPriority : [],\n  blockedList:           Array.isArray(k.blockedList)        ? k.blockedList        : [],\n  overdueList:           Array.isArray(k.overdueList)        ? k.overdueList        : [],\n  assigneeKPIs:          Array.isArray(k.assigneeKPIs)       ? k.assigneeKPIs       : [],\n};\n\nconst statusOverview    = Array.isArray(d.statusOverview)    ? d.statusOverview    : [];\nconst priorityBreakdown = Array.isArray(d.priorityBreakdown) ? d.priorityBreakdown : [];\nconst epicProgress      = Array.isArray(d.epicProgress)      ? d.epicProgress      : [];\nconst recentActivity    = Array.isArray(d.recentActivity)    ? d.recentActivity    : [];\nconst dueSoonIssues     = Array.isArray(d.dueSoonIssues)     ? d.dueSoonIssues     : [];\n\nconst JIRA = $('CONFIGURATION NODE').first().json.jiraBaseUrl + \"/browse\";\nconst NL   = '\\n';\nconst SEP  = '\\n\\n---\\n\\n';\nconst total = metrics.totalIssues || 1;\n\n// \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst daysLabel = sprintDaysLeft === 0 ? 'Sprint ended'\n  : sprintDaysLeft === 1 ? '1 day left'\n  : sprintDaysLeft !== null ? `${sprintDaysLeft} days left` : '';\n\nconst hBar = (pct, w = 18) => {\n  const f = Math.min(w, Math.max(0, Math.round(((pct || 0) / 100) * w)));\n  return '\u2588'.repeat(f) + '\u2591'.repeat(w - f);\n};\n\nconst pIcon = (p) =>\n  ({'Highest':'\ud83d\udd34','High':'\ud83d\udfe0','Medium':'\ud83d\udfe1','Low':'\ud83d\udfe2','Lowest':'\u26aa'})[p] || '\u26aa';\n\nconst sIcon = (s) => {\n  if (!s) return '\u2b1c';\n  const map = {\n    'Done':'\u2705','Termin\u00e9':'\u2705',\n    'In Progress':'\ud83d\udd35','En cours':'\ud83d\udd35',\n    'In Review':'\ud83d\udfe3','En r\u00e9vision':'\ud83d\udfe3','Revue en cours':'\ud83d\udfe3',\n    'To Do':'\u2b1c','\u00c0 faire':'\u2b1c','Nouveau':'\u2b1c',\n    'Blocked':'\ud83d\udd34',\n  };\n  if (map[s]) return map[s];\n  const sl = s.toLowerCase();\n  if (sl.startsWith('termin\u00e9') || sl.startsWith('termine')) return '\u2705';\n  if (sl.startsWith('en cours')) return '\ud83d\udd35';\n  if (sl.includes('r\u00e9vision') || sl.includes('review')) return '\ud83d\udfe3';\n  return '\u2b1c';\n};\n\nconst healthEmoji =\n  ({'On Track':'\ud83d\udfe2','Slight Risk':'\ud83d\udfe1','At Risk':'\ud83d\udd34','Critical':'\ud83d\udd34'})[kpis.sprintHealth] || '\u26aa';\n\nconst jiraLink = (key) => `[${key}](${JIRA}/${key})`;\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// SECTIONS \u2014 same order as HTML email\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n// \u2500\u2500 1. HEADER \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst S1 = [\n  `# \ud83d\udcca ${projectShort}`,\n  `**${projectKey} / Weekly Status Report**  `,\n  `_${dateRange}_`,\n  '',\n  `| | |`,\n  `|---|---|`,\n  `| **Sprint** | ${sprintName} |`,\n  `| **Period** | ${sprintDates} |`,\n  `| **Progress** | ${sprintPct}% elapsed${daysLabel ? ' \u00b7 ' + daysLabel : ''} |`,\n  ...(sprintGoal ? [`| **Goal** | _${sprintGoal}_ |`] : []),\n].join(NL);\n\n// \u2500\u2500 2. KPI STRIP (matches the 4 cards in HTML) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst S2 = [\n  `## \ud83d\udcc8 Weekly Snapshot`,\n  '',\n  `| \u2705 Completed | \u270f\ufe0f Updated | \ud83c\udd95 Created | \u23f0 Due Soon |`,\n  `|---:|---:|---:|---:|`,\n  `| **${metrics.completed}** | **${metrics.updated}** | **${metrics.created}** | **${metrics.dueSoon}** |`,\n  `| _this week_ | _this week_ | _this week_ | _next 7 days_ |`,\n].join(NL);\n\n// \u2500\u2500 3. SPRINT TIMELINE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst S3 = [\n  `## \ud83c\udfc3 Sprint Timeline \u2014 ${sprintName}`,\n  '',\n  `\\`${hBar(sprintPct, 30)}\\` **${sprintPct}%**${daysLabel ? '  \u00b7  ' + daysLabel : ''}`,\n  '',\n  `_${sprintStartFmt} \u2192 ${sprintEndFmt}_`,\n].join(NL);\n\n// \u2500\u2500 4. SPRINT STATUS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst statusRows = statusOverview.map(s => {\n  const pct = Math.round((s.count / total) * 100);\n  return `| ${sIcon(s.status)} ${s.status} | **${s.count}** | ${pct}% | \\`${hBar(pct, 12)}\\` |`;\n}).join(NL) || `| \u2014 | 0 | 0% | \\`${hBar(0, 12)}\\` |`;\n\nconst S4 = [\n  `## \ud83c\udfaf Sprint Status`,\n  '',\n  `| Status | Count | % | Distribution |`,\n  `|--------|------:|--:|--------------|`,\n  statusRows,\n].join(NL);\n\n// \u2500\u2500 5. RECENT ACTIVITY \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst activityLines = recentActivity.length === 0\n  ? '_No activity recorded this week._'\n  : recentActivity.map(a =>\n      `**${jiraLink(a.key)}** \u2014 ${a.summary || ''}  ` + NL +\n      `${sIcon(a.status)} \\`${a.status || '\u2014'}\\` \u00b7 ${pIcon(a.priority)} ${a.priority || '\u2014'} \u00b7 \ud83d\udc64 ${a.assignee || '\u2014'} \u00b7 _${a.updated || '\u2014'}_`\n    ).join(NL + NL);\n\nconst S5 = [\n  `## \ud83d\udd50 Recent Activity \u2014 ${sprintName}`,\n  '',\n  activityLines,\n].join(NL);\n\n// \u2500\u2500 6. PRIORITY DISTRIBUTION \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst maxP = Math.max(...priorityBreakdown.map(p => p.count), 1);\nconst prioRows = priorityBreakdown.map(p =>\n  `| ${pIcon(p.priority)} ${p.priority} | **${p.count}** | \\`${hBar(Math.round((p.count/maxP)*100), 12)}\\` |`\n).join(NL) || '_No data_';\n\nconst S6 = [\n  `## \ud83c\udf9a\ufe0f Priority Distribution`,\n  '',\n  `| Priority | Count | Bar |`,\n  `|----------|------:|-----|`,\n  prioRows,\n].join(NL);\n\n// \u2500\u2500 7. TEAM WORKLOAD \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst teamRows = kpis.assigneeKPIs.length > 0\n  ? kpis.assigneeKPIs.map(a => {\n      const w = Math.round((a.total / total) * 100);\n      return `| ${a.assignee} | **${a.total}** | ${w}% | \\`${hBar(w, 12)}\\` |`;\n    }).join(NL)\n  : '_No data_';\n\nconst S7 = [\n  `## \ud83d\udc65 Team Workload \u2014 ${sprintName}`,\n  '',\n  `| Assignee | Issues | % | Bar |`,\n  `|----------|-------:|--:|-----|`,\n  teamRows,\n].join(NL);\n\n// \u2500\u2500 8. EPIC PROGRESS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst epicLines = epicProgress.length === 0\n  ? '_No epics found in this sprint._'\n  : epicProgress.map(e => {\n      const dp = e.donePct   || 0;\n      const ip = e.inProgPct || 0;\n      const tp = e.todoPct   || 0;\n      const dBar = '\ud83d\udfe9'.repeat(Math.min(10, Math.round(dp / 10)));\n      const pBar = '\ud83d\udfe6'.repeat(Math.min(10, Math.round(ip / 10)));\n      const tBar = '\u2b1c'.repeat(Math.max(0, 10 - Math.round(dp/10) - Math.round(ip/10)));\n      return [\n        `### ${jiraLink(e.key)} \u2014 ${e.name}`,\n        `${dBar}${pBar}${tBar}`,\n        `\u2705 **${dp}%** done \u00b7 \ud83d\udd35 **${ip}%** in progress \u00b7 \u2b1c **${tp}%** to do`,\n      ].join(NL);\n    }).join(NL + NL);\n\nconst S8 = [\n  `## \u26a1 Epic Progress \u2014 ${sprintName}`,\n  '',\n  epicLines,\n].join(NL);\n\n// \u2500\u2500 9. KPI DASHBOARD (mirrors HTML KPI section exactly) \u2500\u2500\u2500\u2500\u2500\u2500\n// 9a. Health & Velocity cards\nconst leadDetail = kpis.avgLeadTime !== null\n  ? `Range: ${kpis.minLeadTime}d \u2013 ${kpis.maxLeadTime}d`\n  : 'No resolved issues yet';\n\nconst kpiCards = [\n  `| KPI | Value | Detail |`,\n  `|-----|------:|--------|`,\n  `| ${healthEmoji} Sprint Health | **${kpis.sprintHealth}** | ${kpis.healthGap > 0 ? kpis.healthGap + '% behind target' : 'Ahead of schedule'} |`,\n  `| \u26a1 Velocity | **${kpis.storyPointsWeek} SP this week** | ${kpis.storyPointsCompleted} SP sprint total \u00b7 ${kpis.storyPointsTotal} SP scope |`,\n  `| \u23f1 Avg Lead Time | **${kpis.avgLeadTime !== null ? kpis.avgLeadTime + 'd' : '\u2014'}** | ${leadDetail} |`,\n  `| \u2705 Completion Rate | **${kpis.completionRate}%** | ${kpis.doneCount} done / ${kpis.inProgressCount} active / ${kpis.toDoCount} to do |`,\n  `| \ud83d\udcc5 Throughput (week) | **${kpis.throughputWeek}** | issues closed this week |`,\n  `| \ud83d\udcc5 Throughput (sprint) | **${kpis.throughputSprint}** | total issues closed in sprint |`,\n  `| \ud83d\udd34 Blocked | **${kpis.blockedCount}** | issues with blocked label |`,\n  `| \u26a0\ufe0f Overdue | **${kpis.overdueCount}** | past due date, not done |`,\n].join(NL);\n\n// 9b. Per-Assignee breakdown (mirrors HTML table columns exactly)\nconst asgHeader = [\n  `| Assignee | Done | Active | To Do | Blocked | SP Done | SP Week | Avg Lead | Done% |`,\n  `|----------|-----:|-------:|------:|--------:|--------:|--------:|---------:|------:|`,\n].join(NL);\n\nconst asgRows = kpis.assigneeKPIs.length > 0\n  ? kpis.assigneeKPIs.map(a =>\n      `| ${a.assignee} | **${a.done}** | ${a.inProgress} | ${a.toDo} | ` +\n      `${a.blocked > 0 ? '\ud83d\udd34 **' + a.blocked + '**' : '\u2014'} | ` +\n      `${a.spDone > 0 ? a.spDone + ' SP' : '\u2014'} | ` +\n      `${(a.spDoneWeek || 0) > 0 ? a.spDoneWeek + ' SP' : '\u2014'} | ` +\n      `${a.avgLeadTime !== null ? a.avgLeadTime + 'd' : '\u2014'} | ` +\n      `${a.completionRate}% |`\n    ).join(NL)\n  : '| _No data_ | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 |';\n\n// 9c. Lead time by priority\nconst ltHeader = [\n  `| Priority | Avg Lead Time | Issues Resolved |`,\n  `|----------|-------------:|----------------:|`,\n].join(NL);\n\nconst ltRows = kpis.leadTimeByPriority.length > 0\n  ? kpis.leadTimeByPriority.map(lt =>\n      `| ${lt.priority} | **${lt.avgDays}d** | ${lt.count} issue${lt.count !== 1 ? 's' : ''} |`\n    ).join(NL)\n  : '| \u2014 | No resolved issues yet | \u2014 |';\n\n// 9d. Blocked list\nconst blockedLines = kpis.blockedList.length === 0\n  ? '_No blocked issues \u2705_'\n  : kpis.blockedList.map(b =>\n      `- **${jiraLink(b.key)}** \u2014 ${b.summary}  ` + NL +\n      `  \ud83d\udc64 ${b.assignee} \u00b7 ${pIcon(b.priority)} ${b.priority}` +\n      (b.daysBlocked !== null ? ` \u00b7 \ud83d\udd34 Blocked for **${b.daysBlocked}d**` : '')\n    ).join(NL);\n\n// 9e. Overdue list\nconst overdueLines = kpis.overdueList.length === 0\n  ? '_No overdue issues \u2705_'\n  : [\n      `| Issue | Summary | Due Date | Days Late | Assignee | Status |`,\n      `|-------|---------|----------|----------:|----------|--------|`,\n      ...kpis.overdueList.map(o =>\n        `| ${jiraLink(o.key)} | ${o.summary} | **${o.duedate}** | \ud83d\udd34 **${o.daysOverdue}d** | ${o.assignee} | ${sIcon(o.status)} ${o.status} |`\n      ),\n    ].join(NL);\n\nconst S9 = [\n  `## \ud83d\udcca KPI Dashboard \u2014 ${sprintName}`,\n  '',\n  `### \u26a1 Health & Velocity`,\n  '',\n  kpiCards,\n  '',\n  `### \ud83d\udc65 Per-Assignee Breakdown`,\n  '',\n  asgHeader,\n  asgRows,\n  '',\n  `### \u23f1 Lead Time by Priority`,\n  '',\n  ltHeader,\n  ltRows,\n  '',\n  `### \ud83d\udd34 Blocked Issues (${kpis.blockedCount})`,\n  '',\n  blockedLines,\n  '',\n  `### \u26a0\ufe0f Overdue Issues (${kpis.overdueCount})`,\n  '',\n  overdueLines,\n].join(NL);\n\n// \u2500\u2500 10. UPCOMING DEADLINES \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst S10 = dueSoonIssues.length === 0 ? null : [\n  `## \u23f0 Upcoming Deadlines`,\n  '',\n  `| Issue | Summary | Priority | Due Date | Assignee |`,\n  `|-------|---------|----------|----------|----------|`,\n  ...dueSoonIssues.map(i =>\n    `| ${jiraLink(i.key)} | ${i.summary || ''} | ${pIcon(i.priority)} ${i.priority || '\u2014'} | \ud83d\udcc5 **${i.duedate || '\u2014'}** | ${i.assignee || '\u2014'} |`\n  ),\n].join(NL);\n\n// \u2500\u2500 11. FOOTER \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst S11 = [\n  `---`,\n  `_Auto-generated by the **n8n Reporting Agent** for **${projectName}**_  `,\n  `_Sprint: ${sprintName} \u00b7 ${sprintDates} \u00b7 Jira \u2014 IG Green Team_  `,\n  `_Reporting period: ${dateRange} \u00b7 Sent every Monday at 08:00_`,\n].join(NL);\n\n// \u2500\u2500 Assemble in same order as HTML \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst sections = [S1, S2, S3, S4, S5, S6, S7, S8, S9, ...(S10 ? [S10] : []), S11];\nconst markdown = sections.join(SEP);\n\nconst filename = `${sprintName.replace(/\\s+/g, '_')}_weekly_report.md`\n  .replace(/[^a-zA-Z0-9_\\-\\.]/g, '');\n\nreturn [{ json: { markdown, filename, reportTitle } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "71e8ef1f-100d-463d-8782-37bec6336d57",
      "name": "Write Markdown File",
      "type": "n8n-nodes-base.code",
      "position": [
        3056,
        6608
      ],
      "parameters": {
        "jsCode": "// Write Markdown File\n// Reads directly from Build Markdown Report output \u2014 no relay through Final Payload.\nconst d = $input.first().json;\nconst markdown    = d.markdown   || '';\nconst filename    = d.filename   || d.mdFilename || 'report.md';\nconst reportTitle = d.reportTitle || '';\n\nif (!markdown) {\n    throw new Error('No markdown in payload \u2014 check Build Markdown Report node.');\n}\n\nconst base64 = Buffer.from(markdown, 'utf-8').toString('base64');\nlet binaryOutput = {};\ntry {\n    const binaryData = await this.helpers.prepareBinaryData(\n        Buffer.from(markdown, 'utf-8'), filename, 'text/markdown; charset=utf-8'\n    );\n    binaryOutput = { report_md: binaryData };\n} catch (_) {}\n\nreturn [{\n    json: { filename, reportTitle, markdown, markdownBase64: base64 },\n    ...(Object.keys(binaryOutput).length > 0 ? { binary: binaryOutput } : {}),\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "9a10c204-2d0e-48cd-b5a1-bcb1490f1831",
      "name": "Convert HTML to PDF",
      "type": "n8n-nodes-htmlcsstopdf.htmlcsstopdf",
      "position": [
        3712,
        6800
      ],
      "parameters": {
        "timeout": 3000,
        "html_content": "={{ $json.fullHtml }}",
        "output_format": "file",
        "dynamic_params": {
          "params": []
        },
        "output_filename": "={{ $input.last().json.projectKey }}_Weekly_Report_{{ $now.format('yyyy-MM-dd') }}"
      },
      "credentials": {
        "htmlcsstopdfApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "9e5d178b-b606-47ba-a0c6-5f472a25f6ee",
      "name": "Send Report Email",
      "type": "n8n-nodes-base.gmail",
      "position": [
        4320,
        6784
      ],
      "parameters": {
        "sendTo": "={{ $json.emailTo }}",
        "message": "={{ $json.html }}",
        "options": {
          "attachmentsUi": {
            "attachmentsBinary": [
              {}
            ]
          },
          "appendAttribution": false
        },
        "subject": "={{ $json.subject }}"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "2baee88e-8309-45b4-9ef5-baea35a59a3a",
      "name": "SUCCESS OPERATIONS -> MOVE TO STORAGE",
      "type": "n8n-nodes-base.merge",
      "position": [
        4720,
        6768
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineByPosition"
      },
      "typeVersion": 3.2
    },
    {
      "id": "a5285890-1741-45cd-9af4-ca57451d131a",
      "name": "Prepare Report Row",
      "type": "n8n-nodes-base.code",
      "position": [
        5040,
        6768
      ],
      "parameters": {
        "jsCode": "// Prepare Sprint Report row \u2014 reads purely from $input\nconst d = $input.first().json;\nreturn [{ json: {\n    project_key:         d.projectKey,\n    project_name:        d.projectName,\n    project_short:       d.projectShort,\n    sprint_name:         d.sprintName,\n    sprint_goal:         d.sprintGoal         || null,\n    sprintStartFmt:      d.sprintStartFmt,\n    sprintEndFmt:        d.sprintEndFmt,\n    week_start:          d.weekStart,\n    week_end:            d.weekEnd,\n    sprint_pct_elapsed:  d.sprintPct,\n    total_issues:        d.metrics?.totalIssues        ?? 0,\n    completed_this_week: d.metrics?.completed          ?? 0,\n    updated_this_week:   d.metrics?.updated            ?? 0,\n    created_this_week:   d.metrics?.created            ?? 0,\n    due_soon:            d.metrics?.dueSoon            ?? 0,\n    html_report:         d.html                        || null,\n    markdown_report:     d.markdown                    || null,\n    subject_line:        d.subject                     || null,\n    status_overview:     JSON.stringify(d.statusOverview    || []),\n    priority_breakdown:  JSON.stringify(d.priorityBreakdown || []),\n    due_soon_issues:     JSON.stringify(d.dueSoonIssues     || []),\n}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "8b11c6c1-00d1-4edf-8b21-83751dbd5c68",
      "name": "PG: Save Sprint Report",
      "type": "n8n-nodes-base.postgres",
      "position": [
        5296,
        6768
      ],
      "parameters": {
        "table": {
          "__rl": true,
          "mode": "list",
          "value": "sprint_reports"
        },
        "schema": {
          "__rl": true,
          "mode": "list",
          "value": "public"
        },
        "columns": {
          "value": {
            "due_soon": "={{ $json.due_soon }}",
            "week_end": "={{ $json.week_end }}",
            "created_at": "={{ $now.toISO() }}",
            "sprint_end": "={{ $json.sprintEndFmt }}",
            "week_start": "={{ $json.week_start }}",
            "html_report": "={{ $json.html_report }}",
            "project_key": "={{ $json.project_key }}",
            "sprint_goal": "={{ $json.sprint_goal }}",
            "sprint_name": "={{ $json.sprint_name }}",
            "project_name": "={{ $json.project_name }}",
            "sprint_start": "={{ $json.sprintStartFmt }}",
            "subject_line": "={{ $json.subject_line }}",
            "total_issues": "={{ $json.total_issues }}",
            "project_short": "={{ $json.project_short }}",
            "due_soon_issues": "={{ { \"data\": JSON.parse($json.due_soon_issues) } }}",
            "markdown_report": "={{ $json.markdown_report }}",
            "status_overview": "={{ { \"data\": JSON.parse($json.status_overview) } }}",
            "created_this_week": "={{ $json.created_this_week }}",
            "updated_this_week": "={{ $json.updated_this_week }}",
            "priority_breakdown": "={{ { \"data\": JSON.parse($json.priority_breakdown) } }}",
            "sprint_pct_elapsed": "={{ $json.sprint_pct_elapsed }}",
            "completed_this_week": "={{ $json.completed_this_week }}"
          },
          "schema": [
            {
              "id": "id",
              "type": "number",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "id",
              "defaultMatch": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "project_key",
              "type": "string",
              "display": true,
              "required": true,
              "displayName": "project_key",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "project_name",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "project_name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "project_short",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "project_short",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "sprint_name",
              "type": "string",
              "display": true,
              "required": true,
              "displayName": "sprint_name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "week_start",
              "type": "dateTime",
              "display": true,
              "required": true,
              "displayName": "week_start",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "week_end",
              "type": "dateTime",
              "display": true,
              "required": true,
              "displayName": "week_end",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "sprint_start",
              "type": "dateTime",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "sprint_start",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "sprint_end",
              "type": "dateTime",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "sprint_end",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "sprint_pct_elapsed",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "sprint_pct_elapsed",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "sprint_goal",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "sprint_goal",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "total_issues",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "total_issues",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "completed_this_week",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "completed_this_week",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "updated_this_week",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "updated_this_week",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "created_this_week",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "created_this_week",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "due_soon",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "due_soon",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "html_report",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "html_report",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "markdown_report",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "markdown_report",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "subject_line",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "subject_line",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "status_overview",
              "type": "object",
              "display": true,
              "required": false,
              "displayName": "status_overview",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "priority_breakdown",
              "type": "object",
              "display": true,
              "required": false,
              "displayName": "priority_breakdown",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "due_soon_issues",
              "type": "object",
              "display": true,
              "required": false,
              "displayName": "due_soon_issues",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "created_at",
              "type": "dateTime",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "created_at",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "project_key",
            "sprint_name",
            "week_start"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.5
    },
    {
      "id": "7073d284-88e4-4301-9d6d-d090c45b617b",
      "name": "WAIT_FOR_REPORT_ID",
      "type": "n8n-nodes-base.merge",
      "position": [
        5568,
        6336
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineAll"
      },
      "typeVersion": 3.2
    },
    {
      "id": "059b3916-01eb-40a0-896d-9d7c4bfdc278",
      "name": "Prepare KPI Row",
      "type": "n8n-nodes-base.code",
      "position": [
        5920,
        6336
      ],
      "parameters": {
        "jsCode": "// Prepare Weekly KPIs row \u2014 reads purely from $input\nconst d = $input.first().json;\nconst k = d.kpis || {};\nreturn [{ json: {\n    report_id:               d.id,\n    project_key:             d.project_key,\n    sprint_name:             d.sprint_name,\n    week_start:              d.week_start,\n    story_points_completed:  k.storyPointsCompleted  ?? 0,\n    story_points_total:      k.storyPointsTotal      ?? 0,\n    story_points_remaining:  k.storyPointsRemaining  ?? 0,\n    completion_rate:         k.completionRate        ?? 0,\n    throughput_week:         k.throughputWeek        ?? 0,\n    throughput_sprint:       k.throughputSprint      ?? 0,\n    sprint_health:           k.sprintHealth          || null,\n    health_gap:              k.healthGap             ?? 0,\n    avg_lead_time:           k.avgLeadTime           ?? null,\n    min_lead_time:           k.minLeadTime           ?? null,\n    max_lead_time:           k.maxLeadTime           ?? null,\n    done_count:              k.doneCount             ?? 0,\n    in_progress_count:       k.inProgressCount       ?? 0,\n    todo_count:              k.toDoCount             ?? 0,\n    blocked_count:           k.blockedCount          ?? 0,\n    overdue_count:           k.overdueCount          ?? 0,\n    lead_time_by_priority:   JSON.stringify(k.leadTimeByPriority || []),\n    blocked_list:            JSON.stringify(k.blockedList        || []),\n    overdue_list:            JSON.stringify(k.overdueList        || []),\n}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "8879a411-2fa5-464b-9613-b1b42bd9f3be",
      "name": "PG: Save Weekly KPIs",
      "type": "n8n-nodes-base.postgres",
      "position": [
        6384,
        6336
      ],
      "parameters": {
        "table": {
          "__rl": true,
          "mode": "list",
          "value": "weekly_kpis"
        },
        "schema": {
          "__rl": true,
          "mode": "list",
          "value": "public"
        },
        "columns": {
          "value": {
            "report_id": "={{ $json.report_id }}",
            "created_at": "={{ $now.toISO() }}",
            "done_count": "={{ $json.done_count }}",
            "health_gap": "={{ $json.health_gap }}",
            "todo_count": "={{ $json.todo_count }}",
            "week_start": "={{ $json.week_start }}",
            "project_key": "={{ $json.project_key }}",
            "sprint_name": "={{ $json.sprint_name }}",
            "blocked_list": "={{ { \"data\": JSON.parse($json.blocked_list) } }}",
            "overdue_list": "={{ { \"data\": JSON.parse($json.overdue_list) } }}",
            "avg_lead_time": "={{ $json.avg_lead_time }}",
            "blocked_count": "={{ $json.blocked_count }}",
            "max_lead_time": "={{ $json.max_lead_time }}",
            "min_lead_time": "={{ $json.min_lead_time }}",
            "overdue_count": "={{ $json.overdue_count }}",
            "sprint_health": "={{ $json.sprint_health }}",
            "completion_rate": "={{ $json.completion_rate }}",
            "throughput_week": "={{ $json.throughput_week }}",
            "in_progress_count": "={{ $json.in_progress_count }}",
            "throughput_sprint": "={{ $json.throughput_sprint }}",
            "story_points_total": "={{ $json.story_points_total }}",
            "lead_time_by_priority": "={{ { \"data\": JSON.parse($json.lead_time_by_priority) } }}",
            "story_points_completed": "={{ $json.story_points_completed }}",
            "story_points_remaining": "={{ $json.story_points_remaining }}"
          },
          "schema": [
            {
              "id": "id",
              "type": "number",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "id",
              "defaultMatch": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "report_id",
              "type": "number",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "report_id",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "project_key",
              "type": "string",
              "display": true,
              "required": true,
              "displayName": "project_key",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "sprint_name",
              "type": "string",
              "display": true,
              "required": true,
              "displayName": "sprint_name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "week_start",
              "type": "dateTime",
              "display": true,
              "required": true,
              "displayName": "week_start",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "story_points_completed",
              "type": "number",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "story_points_completed",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "story_points_total",
              "type": "number",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "story_points_total",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "story_points_remaining",
              "type": "number",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "story_points_remaining",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "completion_rate",
              "type": "number",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "completion_rate",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "throughput_week",
              "type": "number",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "throughput_week",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "throughput_sprint",
              "type": "number",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "throughput_sprint",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "sprint_health",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "sprint_health",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "health_gap",
              "type": "number",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "health_gap",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "avg_lead_time",
              "type": "number",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "avg_lead_time",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "min_lead_time",
              "type": "number",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "min_lead_time",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "max_lead_time",
              "type": "number",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "max_lead_time",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "done_count",
              "type": "number",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "done_count",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "in_progress_count",
              "type": "number",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "in_progress_count",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "todo_count",
              "type": "number",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "todo_count",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "blocked_count",
              "type": "number",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "blocked_count",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "overdue_count",
              "type": "number",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "overdue_count",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "lead_time_by_priority",
              "type": "object",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "lead_time_by_priority",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "blocked_list",
              "type": "object",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "blocked_list",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "overdue_list",
              "type": "object",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "overdue_list",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "created_at",
              "type": "dateTime",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "created_at",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "project_key",
            "sprint_name",
            "week_start"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.5
    },
    {
      "id": "85c26caa-9b88-48e8-bbd0-e5304cd867d9",
      "name": "Loop Over Items",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        6384,
        6528
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "663afdb4-9f0c-450f-8f16-80e3a57d9a5d",
      "name": "Loop Over Items1",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        6384,
        6784
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "8eda5f79-6e63-461c-ad9a-ff711df066fb",
      "name": "No Operation, do nothing",
      "type": "n8n-nodes-base.noOp",
      "position": [
        6688,
        6336
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "8535e818-980c-4148-8f71-f572e7e3fd6c",
      "name": "PG: Save Assignee KPIs",
      "type": "n8n-nodes-base.postgres",
      "position": [
        6688,
        6544
      ],
      "parameters": {
        "table": {
          "__rl": true,
          "mode": "list",
          "value": "assignee_kpis"
        },
        "schema": {
          "__rl": true,
          "mode": "list",
          "value": "public"
        },
        "columns": {
          "value": {
            "done": "={{ $json.done }}",
            "todo": "={{ $json.todo }}",
            "blocked": "={{ $json.blocked }}",
            "assignee": "={{ $json.assignee }}",
            "report_id": "={{ $json.report_id }}",
            "created_at": "={{ $now.toISO() }}",
            "week_start": "={{ $json.week_start }}",
            "in_progress": "={{ $json.in_progress }}",
            "project_key": "={{ $json.project_key }}",
            "sprint_name": "={{ $json.sprint_name }}",
            "total_issues": "={{ $json.total_issues }}",
            "avg_lead_time": "={{ $json.avg_lead_time }}",
            "project_short": "={{ $json.project_short }}",
            "completion_rate": "={{ $json.completion_rate }}",
            "story_points_done": "={{ $json.story_points_done }}",
            "story_points_total": "={{ $json.story_points_total }}"
          },
          "schema": [
            {
              "id": "id",
              "type": "number",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "id",
              "defaultMatch": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "report_id",
              "type": "number",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "report_id",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "project_key",
              "type": "string",
              "display": true,
              "required": true,
              "displayName": "project_key",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "project_short",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "project_short",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "sprint_name",
              "type": "string",
              "display": true,
              "required": true,
              "displayName": "sprint_name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "week_start",
              "type": "dateTime",
              "display": true,
              "required": true,
              "displayName": "week_start",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "assignee",
              "type": "string",
              "display": true,
              "required": true,
              "displayName": "assignee",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "total_issues",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "total_issues",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "done",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "done",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "in_progress",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "in_progress",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "todo",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "todo",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "blocked",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "blocked",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "story_points_done",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "story_points_done",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "story_points_total",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "story_points_total",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "avg_lead_time",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "avg_lead_time",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "completion_rate",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "completion_rate",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "created_at",
              "type": "dateTime",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "created_at",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "project_key",
            "sprint_name",
            "week_start",
            "assignee"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.5
    },
    {
      "id": "4a8b82b8-cc27-4b1e-a04b-d0cc808dec71",
      "name": "PG: Save Epic Snapshots",
      "type": "n8n-nodes-base.postgres",
      "position": [
        6688,
        6800
      ],
      "parameters": {
        "table": {
          "__rl": true,
          "mode": "list",
          "value": "epic_snapshots"
        },
        "schema": {
          "__rl": true,
          "mode": "list",
          "value": "public"
        },
        "columns": {
          "value": {
            "done_pct": "={{ $json.done_pct }}",
            "epic_key": "={{ $json.epic_key }}",
            "todo_pct": "={{ $json.todo_pct }}",
            "epic_name": "={{ $json.epic_name }}",
            "report_id": "={{ $json.report_id }}",
            "created_at": "={{ $now.toISO() }}",
            "week_start": "={{ $json.week_start }}",
            "project_key": "={{ $json.project_key }}",
            "sprint_name": "={{ $json.sprint_name }}",
            "total_issues": "={{ $json.total_issues }}",
            "in_progress_pct": "={{ $json.in_progress_pct }}"
          },
          "schema": [
            {
              "id": "id",
              "type": "number",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "id",
              "defaultMatch": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "report_id",
              "type": "number",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "report_id",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "project_key",
              "type": "string",
              "display": true,
              "required": true,
              "displayName": "project_key",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "sprint_name",
              "type": "string",
              "display": true,
              "required": true,
              "displayName": "sprint_name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "week_start",
              "type": "dateTime",
              "display": true,
              "required": true,
              "displayName": "week_start",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "epic_key",
              "type": "string",
              "display": true,
              "required": true,
              "displayName": "epic_key",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "epic_name",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "epic_name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "total_issues",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "total_issues",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "done_pct",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "done_pct",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "in_progress_pct",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "in_progress_pct",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "todo_pct",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "todo_pct",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "created_at",
              "type": "dateTime",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "created_at",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "project_key",
            "sprint_name",
            "week_start",
            "epic_key"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.5
    },
    {
      "id": "b6a917fc-09de-4110-b7b2-c2bfa306fd04",
      "name": "continue_loop1",
      "type": "n8n-nodes-base.code",
      "position": [
        6992,
        6544
      ],
      "parameters": {
        "jsCode": "return (\n  [\n    {\n      json: {\n        success: \"true\"\n      }\n    }\n  ]\n)"
      },
      "typeVersion": 2
    },
    {
      "id": "6b8b162d-5112-4bab-9d45-465758be9d1e",
      "name": "continue_loop2",
      "type": "n8n-nodes-base.code",
      "position": [
        6992,
        6800
      ],
      "parameters": {
        "jsCode": "return (\n  [\n    {\n      json: {\n        success: \"true\"\n      }\n    }\n  ]\n)"
      },
      "typeVersion": 2
    },
    {
      "id": "579f1981-6ad4-4444-b420-9af501de31f6",
      "name": "Aggregate Report Metrics",
      "type": "n8n-nodes-base.code",
      "position": [
        2464,
        6368
      ],
      "parameters": {
        "jsCode": "// ============================================================\n// AGGREGATE REPORT METRICS\n// Each item has _type set by its Label node upstream.\n// We filter by _type \u2014 deterministic, accurate, rename-proof.\n// ============================================================\n\nconst allItems = $input.all().map(i => i.json);\n\n// \u2500\u2500 Split by _type \u2014 exact, guaranteed \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst updatedIssues   = allItems.filter(i => i._type === 'updated');\nconst createdIssues   = allItems.filter(i => i._type === 'created');\nconst completedIssues = allItems.filter(i => i._type === 'completed');\nconst dueSoonIssues   = allItems.filter(i => i._type === 'dueSoon');\nconst allEpics        = allItems.filter(i => i._type === 'epics');\nconst allIssues       = allItems.filter(i => i._type === 'allIssues');\nconst configItem      = allItems.find(i => i._type === 'config') || {};\n\n// \u2500\u2500 Safe helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst safeRound = (n, d = 1) => {\n    if (!isFinite(n) || isNaN(n)) return 0;\n    return Math.round(n * Math.pow(10, d)) / Math.pow(10, d);\n};\nconst safePct = (num, den) => den > 0 ? Math.round((num / den) * 100) : 0;\nconst daysBetween = (isoA, isoB) => {\n    if (!isoA || !isoB) return null;\n    const diff = new Date(isoB).getTime() - new Date(isoA).getTime();\n    return safeRound(diff / (1000 * 60 * 60 * 24), 1);\n};\n\n// \u2500\u2500 Project identity \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst _proj = allIssues[0]?.fields?.project || {};\nconst projectKey   = configItem.projectKey || _proj.key  || 'PROJECT';\nconst projectName  = _proj.name || projectKey;\nconst projectShort = projectName.replace(/^PFE Project:\\s*/i, '').trim() || projectName;\n\n// \u2500\u2500 Sprint metadata \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst _sprintArr    = allIssues[0]?.fields?.customfield_10020 || [];\nconst _activeSprint = _sprintArr.find(s => s.state === 'active') || _sprintArr[0] || {};\n\nconst sprintName  = _activeSprint.name || 'Current Sprint';\nconst sprintGoal  = _activeSprint.goal || '';\n\nconst fmtLong = (iso) => {\n    if (!iso) return '\u2014';\n    return new Date(iso).toLocaleDateString('en-GB', {\n        day: 'numeric', month: 'long', year: 'numeric'\n    });\n};\n\nconst sprintStart    = _activeSprint.startDate || null;\nconst sprintEnd      = _activeSprint.endDate   || null;\nconst sprintStartFmt = fmtLong(sprintStart);\nconst sprintEndFmt   = fmtLong(sprintEnd);\nconst sprintDates    = (sprintStart && sprintEnd)\n    ? `${sprintStartFmt} \u2014 ${sprintEndFmt}` : (configItem.dateRange || '');\n\nlet sprintPct = 0, sprintDaysLeft = null;\nif (sprintStart && sprintEnd) {\n    const now   = Date.now();\n    const start = new Date(sprintStart).getTime();\n    const end   = new Date(sprintEnd).getTime();\n    const total = end - start;\n    sprintPct      = Math.min(100, Math.round((Math.max(0, now - start) / total) * 100));\n    sprintDaysLeft = Math.max(0, Math.ceil((end - now) / (1000 * 60 * 60 * 24)));\n}\n\n// \u2500\u2500 Classifiers (operate on allIssues \u2014 the status overview set) \u2500\u2500\nconst isDone       = i => i.fields?.status?.statusCategory?.key === 'done';\nconst isInProgress = i => i.fields?.status?.statusCategory?.key === 'indeterminate';\nconst isToDo       = i => i.fields?.status?.statusCategory?.key === 'new';\nconst isBlocked    = i => i.fields?.status?.name === 'BLOCKED';\nconst storyPts     = i => Number(i.fields?.customfield_10016) || 0;\n\nconst doneIssues    = allIssues.filter(isDone);\nconst inProgIssues  = allIssues.filter(isInProgress);\nconst toDoIssues    = allIssues.filter(isToDo);\nconst blockedIssues = allIssues.filter(isBlocked);\nconst totalCount    = allIssues.length || 1;\n\n// \u2500\u2500 Status Overview \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Normalize Jira status names (French \u2192 English) at source\nconst normalizeStatus = (s) => {\n    if (!s) return 'Unknown';\n    // Exact matches first\n    const map = {\n        'Done':          'Done',\n        'Termin\u00e9':       'Done',\n        'En cours':      'In Progress',\n        'In Progress':   'In Progress',\n        'En r\u00e9vision':   'In Review',\n        'Revue en cours':'In Review',\n        'In Review':     'In Review',\n        '\u00c0 faire':       'To Do',\n        'Nouveau':       'To Do',\n        'New':           'To Do',\n        'To Do':         'To Do',\n        'Blocked':       'Blocked',\n    };\n    if (map[s]) return map[s];\n    // Fallback: pattern match for Jira variants like \"Termin\u00e9(e)\", \"Termin\u00e9e\", etc.\n    const sl = s.toLowerCase();\n    if (sl.startsWith('termin\u00e9') || sl.startsWith('termine')) return 'Done';\n    if (sl.startsWith('en cours')) return 'In Progress';\n    if (sl.includes('r\u00e9vision') || sl.includes('review')) return 'In Review';\n    if (sl.startsWith('\u00e0 faire') || sl === 'nouveau' || sl === 'new') return 'To Do';\n    return s;\n};\n\nconst statusCount = {};\nfor (const issue of allIssues) {\n    const s = normalizeStatus(issue.fields?.status?.name || 'Unknown');\n    statusCount[s] = (statusCount[s] || 0) + 1;\n}\nconst statusOverview = Object.entries(statusCount)\n    .map(([status, count]) => ({ status, count }))\n    .sort((a, b) => b.count - a.count);\n\n// \u2500\u2500 Priority Breakdown \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst priorityCount = {};\nfor (const issue of allIssues) {\n    const p = issue.fields?.priority?.name || 'None';\n    priorityCount[p] = (priorityCount[p] || 0) + 1;\n}\nconst priorityBreakdown = Object.entries(priorityCount)\n    .map(([priority, count]) => ({ priority, count }))\n    .sort((a, b) => b.count - a.count);\n\n// \u2500\u2500 Epic Progress \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst epicProgress = allEpics.slice(0, 6).map(epic => {\n    const epicKey  = epic.key;\n    const epicName = epic.fields?.summary || epicKey;\n    const children = allIssues.filter(i =>\n        i.fields?.parent?.key === epicKey || i.fields?.customfield_10014 === epicKey\n    );\n    const done   = children.filter(isDone).length;\n    const inProg = children.filter(isInProgress).length;\n    const toDo   = children.filter(isToDo).length;\n    const ct     = children.length || 1;\n    return {\n        key: epicKey, name: epicName,\n        status: epic.fields?.status?.name,\n        donePct:   Math.round((done   / ct) * 100),\n        inProgPct: Math.round((inProg / ct) * 100),\n        todoPct:   Math.round((toDo   / ct) * 100),\n        total: children.length\n    };\n});\n\n// \u2500\u2500 Recent Activity (top 5 from updatedIssues \u2014 already sorted by Jira) \u2500\u2500\nconst recentActivity = updatedIssues.slice(0, 5).map(i => ({\n    key:      i.key,\n    summary:  i.fields?.summary?.substring(0, 80),\n    status:   normalizeStatus(i.fields?.status?.name || 'Unknown'),\n    priority: i.fields?.priority?.name || 'None',\n    assignee: i.fields?.assignee?.displayName || 'Unassigned',\n    updated:  i.fields?.updated?.substring(0, 10),\n    type:     i.fields?.issuetype?.name\n}));\n\n// \u2500\u2500 Team Workload \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst assigneeCount = {};\nfor (const issue of allIssues) {\n    const a = issue.fields?.assignee?.displayName || 'Unassigned';\n    assigneeCount[a] = (assigneeCount[a] || 0) + 1;\n}\nconst teamWorkload = Object.entries(assigneeCount)\n    .map(([assignee, count]) => ({ assignee, count, pct: safePct(count, totalCount) }))\n    .sort((a, b) => b.count - a.count);\n\n// \u2500\u2500 KPI: Velocity & Story Points \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst spVelocity      = doneIssues.reduce((s, i) => s + storyPts(i), 0);\nconst spTotal         = allIssues.reduce((s, i) => s + storyPts(i), 0);\nconst spRemaining     = inProgIssues.concat(toDoIssues).reduce((s, i) => s + storyPts(i), 0);\nconst spCompletedWeek = completedIssues.reduce((s, i) => s + storyPts(i), 0);\n\n// \u2500\u2500 KPI: Completion Rate \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst completionRate = safePct(doneIssues.length, totalCount);\n\n// \u2500\u2500 KPI: Sprint Health \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst healthGap    = sprintPct - completionRate;\nconst sprintHealth = healthGap <= 0  ? 'On Track'\n    : healthGap <= 20 ? 'Slight Risk'\n    : healthGap <= 40 ? 'At Risk'\n    : 'Critical';\nconst sprintHealthScore = Math.max(0, Math.min(100, Math.round(100 - healthGap * 1.5)));\n\n// \u2500\u2500 KPI: Lead Time \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst leadTimes = doneIssues\n    .map(i => daysBetween(i.fields?.created, i.fields?.resolutiondate))\n    .filter(d => d !== null && d >= 0);\nconst avgLeadTime = leadTimes.length > 0\n    ? safeRound(leadTimes.reduce((s, d) => s + d, 0) / leadTimes.length, 1) : null;\nconst minLeadTime = leadTimes.length > 0 ? Math.min(...leadTimes) : null;\nconst maxLeadTime = leadTimes.length > 0 ? Math.max(...leadTimes) : null;\n\n// \u2500\u2500 KPI: Lead Time by Priority \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst ltByPriority = {};\nfor (const issue of doneIssues) {\n    const p  = issue.fields?.priority?.name || 'None';\n    const lt = daysBetween(issue.fields?.created, issue.fields?.resolutiondate);\n    if (lt !== null && lt >= 0) {\n        if (!ltByPriority[p]) ltByPriority[p] = [];\n        ltByPriority[p].push(lt);\n    }\n}\nconst leadTimeByPriority = Object.entries(ltByPriority).map(([priority, times]) => ({\n    priority,\n    avgDays: safeRound(times.reduce((s, t) => s + t, 0) / times.length, 1),\n    count:   times.length\n})).sort((a, b) => {\n    const order = ['Highest', 'High', 'Medium', 'Low', 'Lowest'];\n    return order.indexOf(a.priority) - order.indexOf(b.priority);\n});\n\n// \u2500\u2500 KPI: Blocked Issues \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst blockedList = blockedIssues.map(i => ({\n    key:      i.key,\n    summary:  i.fields?.summary?.substring(0, 70),\n    assignee: i.fields?.assignee?.displayName || 'Unassigned',\n    priority: i.fields?.priority?.name || 'None',\n    daysBlocked: (() => {\n        const changed = i.fields?.statuscategorychangedate;\n        if (!changed) return null;\n        return Math.floor((Date.now() - new Date(changed).getTime()) / (1000 * 60 * 60 * 24));\n    })()\n}));\n\n// \u2500\u2500 KPI: Overdue Issues \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst today = new Date(); today.setHours(0, 0, 0, 0);\nconst overdueList = allIssues\n    .filter(i => {\n        if (isDone(i)) return false;\n        const due = i.fields?.duedate;\n        if (!due) return false;\n        return new Date(due) < today;\n    })\n    .map(i => ({\n        key:         i.key,\n        summary:     i.fields?.summary?.substring(0, 70),\n        duedate:     i.fields?.duedate,\n        assignee:    i.fields?.assignee?.displayName || 'Unassigned',\n        priority:    i.fields?.priority?.name || 'None',\n        status:      normalizeStatus(i.fields?.status?.name || 'Unknown'),\n        daysOverdue: Math.floor(\n            (today.getTime() - new Date(i.fields.duedate).getTime()) / (1000 * 60 * 60 * 24)\n        )\n    }))\n    .sort((a, b) => b.daysOverdue - a.daysOverdue);\n\n// \u2500\u2500 KPI: Per-Assignee Breakdown \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Pre-compute SP done this week per assignee from completedIssues\nconst spWeekByAssignee = {};\nfor (const issue of completedIssues) {\n    const a = issue.fields?.assignee?.displayName || 'Unassigned';\n    spWeekByAssignee[a] = (spWeekByAssignee[a] || 0) + storyPts(issue);\n}\n\nconst assigneeMap = {};\nfor (const issue of allIssues) {\n    const a = issue.fields?.assignee?.displayName || 'Unassigned';\n    if (!assigneeMap[a]) assigneeMap[a] = {\n        done: 0, inProgress: 0, toDo: 0, blocked: 0,\n        sp: 0, spDone: 0, leadTimes: []\n    };\n    const m = assigneeMap[a];\n    if (isDone(issue))          { m.done++;       m.spDone += storyPts(issue); }\n    else if (isInProgress(issue)) m.inProgress++;\n    else if (isToDo(issue))       m.toDo++;\n    if (isBlocked(issue))         m.blocked++;\n    m.sp += storyPts(issue);\n    const lt = daysBetween(issue.fields?.created, issue.fields?.resolutiondate);\n    if (lt !== null && lt >= 0 && isDone(issue)) m.leadTimes.push(lt);\n}\nconst assigneeKPIs = Object.entries(assigneeMap).map(([assignee, m]) => {\n    const assigneeTotal = m.done + m.inProgress + m.toDo;\n    return {\n        assignee,\n        total:          assigneeTotal,\n        done:           m.done,\n        inProgress:     m.inProgress,\n        toDo:           m.toDo,\n        blocked:        m.blocked,\n        completionRate: safePct(m.done, assigneeTotal || 1),\n        storyPoints:    m.sp,\n        spDone:         m.spDone,\n        spDoneWeek:     spWeekByAssignee[assignee] || 0,\n        avgLeadTime:    m.leadTimes.length > 0\n            ? safeRound(m.leadTimes.reduce((s, t) => s + t, 0) / m.leadTimes.length, 1)\n            : null,\n        pct: safePct(assigneeTotal, totalCount)\n    };\n}).sort((a, b) => b.total - a.total);\n\n// \u2500\u2500 KPI: Type Breakdown \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst typeMap = {};\nfor (const issue of allIssues) {\n    const t = issue.fields?.issuetype?.name || 'Unknown';\n    if (!typeMap[t]) typeMap[t] = { total: 0, done: 0, sp: 0 };\n    typeMap[t].total++;\n    if (isDone(issue)) typeMap[t].done++;\n    typeMap[t].sp += storyPts(issue);\n}\nconst typeBreakdown = Object.entries(typeMap)\n    .map(([type, m]) => ({\n        type, total: m.total, done: m.done,\n        donePct: safePct(m.done, m.total),\n        sp: m.sp, pct: safePct(m.total, totalCount)\n    }))\n    .sort((a, b) => b.total - a.total);\n\n// \u2500\u2500 Assemble \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst kpis = {\n    storyPointsCompleted: spVelocity,\n    storyPointsTotal:     spTotal,\n    storyPointsRemaining: spRemaining,\n    storyPointsWeek:      spCompletedWeek,\n    completionRate,\n    throughputWeek:       completedIssues.length,\n    throughputSprint:     doneIssues.length,\n    sprintHealth, sprintHealthScore, healthGap,\n    avgLeadTime, minLeadTime, maxLeadTime,\n    leadTimeByPriority,\n    doneCount:       doneIssues.length,\n    inProgressCount: inProgIssues.length,\n    toDoCount:       toDoIssues.length,\n    blockedCount:    blockedIssues.length,\n    overdueCount:    overdueList.length,\n    blockedList, overdueList, assigneeKPIs, typeBreakdown\n};\n\nreturn [{\n    json: {\n        projectKey,\n        emailTo:      configItem.emailTo      || '',\n\n        weekStart:    configItem.weekStart     || '',\n        weekEnd:      configItem.weekEnd       || '',\n        projectName, projectShort,\n        sprintName, sprintGoal, sprintDates,\n        sprintStartFmt, sprintEndFmt,\n        sprintPct, sprintDaysLeft,\n        reportTitle: `${sprintName} \u2014 Weekly Report \u2014 ${projectShort}`,\n        weekLabel:   configItem.weekLabel || '',\n        today:       configItem.today     || '',\n        dateRange:   configItem.dateRange || sprintDates,\n        metrics: {\n            completed:   completedIssues.length,\n            updated:     updatedIssues.length,\n            created:     createdIssues.length,\n            dueSoon:     dueSoonIssues.length,\n            totalIssues: allIssues.length\n        },\n        kpis,\n        statusOverview, priorityBreakdown,\n        teamWorkload, epicProgress, recentActivity,\n        dueSoonIssues: dueSoonIssues.slice(0, 5).map(i => ({\n            key:      i.key,\n            summary:  i.fields?.summary?.substring(0, 70),\n            duedate:  i.fields?.duedate,\n            assignee: i.fields?.assignee?.displayName || 'Unassigned',\n            priority: i.fields?.priority?.name\n        }))\n    }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "2356a590-360a-4c07-83c8-56871dc268f7",
      "name": "JIRA_DATA_LOADING",
      "type": "n8n-nodes-base.merge",
      "position": [
        2208,
        6288
      ],
      "parameters": {
        "numberInputs": 7
      },
      "typeVersion": 3.2
    },
    {
      "id": "2911b234-7ad9-4d60-b52c-34df3db5df93",
      "name": "Every Monday 8AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "disabled": true,
      "position": [
        208,
        5920
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * 1"
            }
          ]
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "d650d73c-ed05-4a29-9dce-59ff9b907e9b",
      "name": "WAIT_BUILDS",
      "type": "n8n-nodes-base.merge",
      "position": [
        3456,
        6288
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineByPosition",
        "numberInputs": 4
      },
      "typeVersion": 3.2
    },
    {
      "id": "f22a55ae-1086-406f-97a9-fd2896ec83b6",
      "name": "Build Final Payload",
      "type": "n8n-nodes-base.code",
      "position": [
        3712,
        6320
      ],
      "parameters": {
        "jsCode": "// Build Final Payload\n// Combine Outputs passes 1 merged item containing everything.\nconst d = $input.first().json;\nreturn [{ json: {\n    projectKey:    d.projectKey,\n    projectName:   d.projectName,\n    projectShort:  d.projectShort,\n    emailTo:       d.emailTo,\n\n    weekStart:     d.weekStart,\n    weekEnd:       d.weekEnd,\n    sprintName:    d.sprintName,\n    sprintGoal:    d.sprintGoal,\n    sprintDates:   d.sprintDates,\n    sprintStartFmt:d.sprintStartFmt,\n    sprintEndFmt:  d.sprintEndFmt,\n    sprintPct:     d.sprintPct,\n    sprintDaysLeft:d.sprintDaysLeft,\n    reportTitle:   d.reportTitle,\n    dateRange:     d.dateRange,\n    weekLabel:     d.weekLabel,\n    today:         d.today,\n    metrics:           d.metrics,\n    kpis:              d.kpis,\n    statusOverview:    d.statusOverview,\n    priorityBreakdown: d.priorityBreakdown,\n    teamWorkload:      d.teamWorkload,\n    epicProgress:      d.epicProgress,\n    recentActivity:    d.recentActivity,\n    dueSoonIssues:     d.dueSoonIssues,\n    html:         d.html       || null,\n    subject:      d.subject    || null,\n    fullHtml:     d.fullHtml   || null,\n    markdown:     d.markdown   || null,\n    mdFilename:   d.filename   || d.mdFilename || null,\n}}];"
      },
      "typeVersion": 2
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "",
  "connections": {
    "WAIT_BUILDS": {
      "main": [
        [
          {
            "node": "Build Final Payload",
            "type": "main",
            "index": 0
          },
          {
            "node": "Convert HTML to PDF",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Label Epics3": {
      "main": [
        [
          {
            "node": "JIRA_DATA_LOADING",
            "type": "main",
            "index": 4
          }
        ]
      ]
    },
    "CREATE TABLES": {
      "main": [
        [
          {
            "node": "CONFIGURATION NODE",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get All Epics": {
      "main": [
        [
          {
            "node": "Label Epics3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Labels Config": {
      "main": [
        [
          {
            "node": "JIRA_DATA_LOADING",
            "type": "main",
            "index": 6
          }
        ]
      ]
    },
    "Label Created3": {
      "main": [
        [
          {
            "node": "JIRA_DATA_LOADING",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Label Updated3": {
      "main": [
        [
          {
            "node": "JIRA_DATA_LOADING",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "continue_loop1": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "continue_loop2": {
      "main": [
        [
          {
            "node": "Loop Over Items1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Full HTML": {
      "main": [
        [
          {
            "node": "WAIT_BUILDS",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Label Due Soon3": {
      "main": [
        [
          {
            "node": "JIRA_DATA_LOADING",
            "type": "main",
            "index": 3
          }
        ]
      ]
    },
    "Loop Over Items": {
      "main": [
        [
          {
            "node": "No Operation, do nothing",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "PG: Save Assignee KPIs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare KPI Row": {
      "main": [
        [
          {
            "node": "PG: Save Weekly KPIs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Email HTML": {
      "main": [
        [
          {
            "node": "HTML",
            "type": "main",
            "index": 0
          },
          {
            "node": "WAIT_BUILDS",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Every Monday 8AM": {
      "main": [
        [
          {
            "node": "CREATE TABLES",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Label Completed3": {
      "main": [
        [
          {
            "node": "JIRA_DATA_LOADING",
            "type": "main",
            "index": 2
          }
        ]
      ]
    },
    "Loop Over Items1": {
      "main": [
        [
          {
            "node": "No Operation, do nothing",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "PG: Save Epic Snapshots",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JIRA_DATA_LOADING": {
      "main": [
        [
          {
            "node": "Aggregate Report Metrics",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Label All Issues3": {
      "main": [
        [
          {
            "node": "JIRA_DATA_LOADING",
            "type": "main",
            "index": 5
          }
        ]
      ]
    },
    "Send Report Email": {
      "main": [
        [
          {
            "node": "SUCCESS OPERATIONS -> MOVE TO STORAGE",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "CONFIGURATION NODE": {
      "main": [
        [
          {
            "node": "Labels Config",
            "type": "main",
            "index": 0
          },
          {
            "node": "Get All Issues (Status Overview)",
            "type": "main",
            "index": 0
          },
          {
            "node": "Get All Epics",
            "type": "main",
            "index": 0
          },
          {
            "node": "Get Due Soon Issues",
            "type": "main",
            "index": 0
          },
          {
            "node": "Get Completed Issues (7d)",
            "type": "main",
            "index": 0
          },
          {
            "node": "Get Created Issues (7d)",
            "type": "main",
            "index": 0
          },
          {
            "node": "Get Updated Issues (7d)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Epic Rows1": {
      "main": [
        [
          {
            "node": "Loop Over Items1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Report Row": {
      "main": [
        [
          {
            "node": "PG: Save Sprint Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "WAIT_FOR_REPORT_ID": {
      "main": [
        [
          {
            "node": "Prepare KPI Row",
            "type": "main",
            "index": 0
          },
          {
            "node": "Prepare Assignee Rows1",
            "type": "main",
            "index": 0
          },
          {
            "node": "Prepare Epic Rows1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Final Payload": {
      "main": [
        [
          {
            "node": "WAIT_FOR_BODY_ATTACHEMENT",
            "type": "main",
            "index": 0
          },
          {
            "node": "SUCCESS OPERATIONS -> MOVE TO STORAGE",
            "type": "main",
            "index": 0
          },
          {
            "node": "WAIT_FOR_REPORT_ID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Convert HTML to PDF": {
      "main": [
        [
          {
            "node": "WAIT_FOR_BODY_ATTACHEMENT",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Get Due Soon Issues": {
      "main": [
        [
          {
            "node": "Label Due Soon3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write Markdown File": {
      "main": [
        [
          {
            "node": "WAIT_BUILDS",
            "type": "main",
            "index": 2
          }
        ]
      ]
    },
    "Build Markdown Report": {
      "main": [
        [
          {
            "node": "Write Markdown File",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "PG: Save Assignee KPIs": {
      "main": [
        [
          {
            "node": "continue_loop1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "PG: Save Sprint Report": {
      "main": [
        [
          {
            "node": "WAIT_FOR_REPORT_ID",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Prepare Assignee Rows1": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Created Issues (7d)": {
      "main": [
        [
          {
            "node": "Label Created3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Updated Issues (7d)": {
      "main": [
        [
          {
            "node": "Label Updated3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "PG: Save Epic Snapshots": {
      "main": [
        [
          {
            "node": "continue_loop2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate Report Metrics": {
      "main": [
        [
          {
            "node": "Build Markdown Report",
            "type": "main",
            "index": 0
          },
          {
            "node": "Build Email HTML",
            "type": "main",
            "index": 0
          },
          {
            "node": "Build Full HTML",
            "type": "main",
            "index": 0
          },
          {
            "node": "WAIT_BUILDS",
            "type": "main",
            "index": 3
          }
        ]
      ]
    },
    "Get Completed Issues (7d)": {
      "main": [
        [
          {
            "node": "Label Completed3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "WAIT_FOR_BODY_ATTACHEMENT": {
      "main": [
        [
          {
            "node": "Send Report Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get All Issues (Status Overview)": {
      "main": [
        [
          {
            "node": "Label All Issues3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When clicking \u2018Execute workflow\u2019": {
      "main": [
        [
          {
            "node": "CREATE TABLES",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SUCCESS OPERATIONS -> MOVE TO STORAGE": {
      "main": [
        [
          {
            "node": "Prepare Report Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}