{
  "name": "JIRA copy",
  "nodes": [
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        176,
        -64
      ],
      "id": "d06c7c09-28e8-4706-8585-0c205b5dfc9e",
      "name": "When clicking 'Execute workflow'"
    },
    {
      "parameters": {
        "url": "https://gupy-io.atlassian.net/rest/agile/1.0/board/3128/sprint",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "jiraSoftwareCloudApi",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "state",
              "value": "closed"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        368,
        -64
      ],
      "id": "d53b6ef6-4a0a-4052-942b-ba92156199a3",
      "name": "1. Buscar Sprints Fechadas",
      "credentials": {
        "jiraSoftwareCloudApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const vals = $json.values || [];\nconst d = x => new Date(x).toISOString().slice(0,10);\nconst today = new Date().toISOString().slice(0,10);\nlet s = vals.find(v => (v.completeDate && d(v.completeDate) === today) || (v.endDate && d(v.endDate) === today));\nif (!s) s = vals.sort((a, b) => new Date(b.endDate) - new Date(a.endDate))[0];\nif (!s) throw new Error('Nenhuma sprint fechada encontrada.');\nreturn [{json: {sprintId: s.id, sprintName: s.name, sprintStartDate: s.startDate, sprintEndDate: s.endDate, sprintCompleteDate: s.completeDate || s.endDate, sprintGoal: s.goal || ''}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        576,
        -64
      ],
      "id": "6e5cb78e-7870-4593-a0a1-093abb4c436a",
      "name": "2. Selecionar Sprint"
    },
    {
      "parameters": {
        "operation": "getAll",
        "returnAll": true,
        "options": {
          "jql": "=Sprint = {{ $json.sprintId }}"
        }
      },
      "type": "n8n-nodes-base.jira",
      "typeVersion": 1,
      "position": [
        768,
        -64
      ],
      "id": "6507c48a-acf1-4d16-8765-38f501fde9ed",
      "name": "3. Buscar Issues da Sprint (Jira)",
      "credentials": {
        "jiraSoftwareCloudApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Busca dados da sprint do node \"2. Selecionar Sprint\"\nconst sprintNode = $('2. Selecionar Sprint');\nconst sprintData = {\n  sprintId: sprintNode.item.json.sprintId,\n  sprintName: sprintNode.item.json.sprintName,\n  sprintStartDate: sprintNode.item.json.sprintStartDate,\n  sprintEndDate: sprintNode.item.json.sprintEndDate,\n  sprintCompleteDate: sprintNode.item.json.sprintCompleteDate,\n  sprintGoal: sprintNode.item.json.sprintGoal\n};\n\n// Retorna um item por issue key com dados da sprint\nreturn $input.all().map(item => ({\n  json: {\n    issueKey: item.json.key,\n    ...sprintData\n  }\n}));"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        976,
        -64
      ],
      "id": "4953b222-ae34-4e48-890a-8acb84267553",
      "name": "4. Preparar Lista de Issues"
    },
    {
      "parameters": {
        "url": "=https://gupy-io.atlassian.net/rest/api/3/issue/{{$json.issueKey}}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "jiraSoftwareCloudApi",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "expand",
              "value": "changelog,renderedFields"
            },
            {
              "name": "fields",
              "value": "summary,description,status,project,assignee,reporter,labels,created,updated,resolutiondate,issuetype,priority,parent,subtasks,duedate,comment"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1184,
        -64
      ],
      "id": "8595f398-6374-4d4d-ab73-032f4d9d7135",
      "name": "5. Buscar Detalhes Completos",
      "credentials": {
        "jiraSoftwareCloudApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Busca dados da sprint do node \"2. Selecionar Sprint\"\nconst sprintNode = $('2. Selecionar Sprint');\n\nif (!sprintNode || !sprintNode.item || !sprintNode.item.json) {\n  throw new Error('N\u00e3o foi poss\u00edvel acessar dados da sprint do node \"2. Selecionar Sprint\"');\n}\n\nconst sprint = sprintNode.item.json;\nconst sprintData = {\n  sprintId: sprint.sprintId,\n  sprintName: sprint.sprintName,\n  sprintStartDate: sprint.sprintStartDate,\n  sprintEndDate: sprint.sprintEndDate,\n  sprintCompleteDate: sprint.sprintCompleteDate,\n  sprintGoal: sprint.sprintGoal || ''\n};\n\n// Verifica se temos os dados essenciais\nif (!sprintData.sprintId || !sprintData.sprintName) {\n  throw new Error('Dados da sprint incompletos: ' + JSON.stringify(sprintData));\n}\n\n// Pega todas as issues do step anterior\nconst allItems = $input.all();\nconst issues = allItems.map(item => item.json).filter(item => item.key);\n\nif (issues.length === 0) {\n  throw new Error('Nenhuma issue encontrada no input');\n}\n\n// Retorna um \u00fanico objeto com dados da sprint + array de issues\nconst result = {\n  ...sprintData,\n  total_issues: issues.length,\n  issues: issues\n};\n\n// Log para debug\nconsole.log('Sprint ID:', result.sprintId);\nconsole.log('Total Issues:', result.total_issues);\n\nreturn [{json: result}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1376,
        -64
      ],
      "id": "7a675d89-fde2-4d84-9a01-2fb1dd378df6",
      "name": "6. Consolidar Issues"
    },
    {
      "parameters": {
        "jsCode": "// ============================================\n// HELPERS DE TEXTO E ARRAYS\n// ============================================\n\n// Converte descri\u00e7\u00e3o ADF para texto simples\nfunction adfToText(desc) {\n  if (!desc) return '';\n  if (typeof desc === 'string') return desc;\n  try {\n    const walk = (n) => {\n      if (!n) return '';\n      if (n.type === 'text') return n.text || '';\n      if (Array.isArray(n.content)) {\n        return n.content.map(walk).join(n.type === 'paragraph' ? '\\n' : '');\n      }\n      return '';\n    };\n    return walk(desc).trim();\n  } catch {\n    return '';\n  }\n}\n\n// Estat\u00edsticas simples (percentis sobre valores num\u00e9ricos)\nfunction stats(arr) {\n  const v = arr.filter(x => Number.isFinite(x)).sort((a, b) => a - b);\n  if (!v.length) return { p50: null, p90: null, p95: null, avg: null, min: null, max: null };\n  const mid = Math.floor(v.length / 2);\n  const p50 = v.length % 2 ? v[mid] : (v[mid - 1] + v[mid]) / 2;\n  const p90 = v[Math.max(0, Math.ceil(v.length * 0.9) - 1)];\n  const p95 = v[Math.max(0, Math.ceil(v.length * 0.95) - 1)];\n  const avg = v.reduce((a, b) => a + b, 0) / v.length;\n  return { p50, p90, p95, avg, min: v[0], max: v[v.length - 1] };\n}\n\n// Normaliza strings (acentos, caixa, espa\u00e7os)\nfunction norm(s) {\n  return (s || '')\n    .toString()\n    .normalize('NFD')\n    .replace(/\\p{Diacritic}/gu, '')\n    .toLowerCase()\n    .trim();\n}\n\n// ============================================\n// DIAS \u00daTEIS (exclui s\u00e1b/dom) \u2014 retorna dias decimais\n// ============================================\n\n// avan\u00e7a para o pr\u00f3ximo dia \u00fatil \u00e0s 00:00 se cair em fim de semana\nfunction nextBusinessStart(d) {\n  const x = new Date(d);\n  while (x.getDay() === 0 || x.getDay() === 6) { // 0=Dom, 6=S\u00e1b\n    x.setDate(x.getDate() + 1);\n    x.setHours(0,0,0,0);\n  }\n  return x;\n}\n// retrocede para o \u00faltimo dia \u00fatil \u00e0s 23:59:59.999 se cair em fim de semana\nfunction prevBusinessEnd(d) {\n  const x = new Date(d);\n  while (x.getDay() === 0 || x.getDay() === 6) {\n    x.setDate(x.getDate() - 1);\n    x.setHours(23,59,59,999);\n  }\n  return x;\n}\n\n// diferen\u00e7a em dias \u00fateis (aceita quaisquer hor\u00e1rios; assume jornada 24h para simplificar a fra\u00e7\u00e3o do dia)\nfunction businessDiffDays(a, b) {\n  if (!a || !b) return null;\n  let start = new Date(a);\n  let end = new Date(b);\n  if (end <= start) return 0;\n\n  start = nextBusinessStart(start);\n  end = prevBusinessEnd(end);\n  if (end <= start) return 0;\n\n  // conta dias inteiros \u00fateis\n  let days = 0;\n  const cur = new Date(start);\n  cur.setHours(0,0,0,0);\n  end.setHours(23,59,59,999);\n\n  while (cur <= end) {\n    const dow = cur.getDay();\n    if (dow !== 0 && dow !== 6) days += 1;\n    cur.setDate(cur.getDate() + 1);\n  }\n\n  // Ajuste fino de fra\u00e7\u00f5es do primeiro/\u00faltimo dia\n  // (mantemos simples: se inicio/fim no meio do dia \u00fatil, aproximamos por propor\u00e7\u00e3o linear 24h)\n  // Primeiro dia\n  const firstDayStart = new Date(start); firstDayStart.setHours(0,0,0,0);\n  const firstDayEnd = new Date(start); firstDayEnd.setHours(23,59,59,999);\n  if (firstDayStart.getDay() !== 0 && firstDayStart.getDay() !== 6) {\n    const frac = (start - firstDayStart) / (24*3600*1000);\n    days -= frac;\n  }\n\n  // \u00daltimo dia\n  const lastDayStart = new Date(end); lastDayStart.setHours(0,0,0,0);\n  const lastDayEnd = new Date(end); lastDayEnd.setHours(23,59,59,999);\n  if (lastDayStart.getDay() !== 0 && lastDayStart.getDay() !== 6) {\n    const frac = (lastDayEnd - end) / (24*3600*1000);\n    days -= frac;\n  }\n\n  return Math.max(0, Number(days));\n}\n\n// ============================================\n// DEFINI\u00c7\u00c3O DO TEU FLUXO (ajuste livre se renomear colunas)\n// ============================================\n\n// Done (categorias/status de conclus\u00e3o)\nconst DONE_STATUSES = [\n  'done','concluido','conclu\u00eddo','fechado','closed','itens concluidos','itens conclu\u00eddos'\n].map(norm);\n\n// Status de prepara\u00e7\u00e3o / discovery / backlog\nconst PREP_STATUSES = [\n  'to do','priorizado','tarefas pendentes',\n  'ideias e insights',\n  'exploracao e analise','explora\u00e7\u00e3o e an\u00e1lise',\n  'prototipagem e validacao','prototipagem e valida\u00e7\u00e3o',\n  'design review',\n  'refinamento tecnico','refinamento t\u00e9cnico',\n  'pronto para desenvolvimento','pronto para desenvolver'\n].map(norm);\n\n// Status de trabalho ativo (delivery)\nconst WORK_STATUSES = [\n  'in progress','em andamento',\n  'code review',\n  'validacao','valida\u00e7\u00e3o',\n  'aceitacao','aceita\u00e7\u00e3o',\n  'pronto para deploy'\n].map(norm);\n\n// Mapeamento de status \u2192 fase macro\nfunction phaseOfStatus(sNorm) {\n  if (['ideias e insights','exploracao e analise','explora\u00e7\u00e3o e an\u00e1lise','prototipagem e validacao','prototipagem e valida\u00e7\u00e3o','design review','refinamento tecnico','refinamento t\u00e9cnico'].includes(sNorm)) {\n    return 'discovery';\n  }\n  if (['priorizado','tarefas pendentes','pronto para desenvolvimento','pronto para desenvolver','to do'].includes(sNorm)) {\n    return 'backlog';\n  }\n  if (['in progress','em andamento'].includes(sNorm)) return 'build';\n  if (['code review','design review'].includes(sNorm)) return 'review';\n  if (['validacao','valida\u00e7\u00e3o','aceitacao','aceita\u00e7\u00e3o'].includes(sNorm)) return 'validation';\n  if (['pronto para deploy'].includes(sNorm)) return 'deploy';\n  if (DONE_STATUSES.includes(sNorm)) return 'done';\n  return 'other';\n}\n\n// ============================================\n// HELPERS COM CHANGELOG\n// ============================================\n\n// Extrai todas transi\u00e7\u00f5es de status ordenadas\nfunction extractStatusEvents(changelog) {\n  if (!changelog?.histories) return [];\n  const events = [];\n  for (const h of changelog.histories) {\n    const at = new Date(h.created);\n    for (const it of h.items || []) {\n      if (it.field === 'status') {\n        events.push({\n          at,\n          from: norm(it.fromString || it.from),\n          to: norm(it.toString || it.to)\n        });\n      }\n    }\n  }\n  events.sort((a,b) => a.at - b.at);\n  return events;\n}\n\n// Primeiro momento em que a issue entrou em algum status do conjunto alvo\nfunction firstTransitionToNormalized(changelog, targetSet) {\n  const events = extractStatusEvents(changelog);\n  const hit = events.find(e => targetSet.includes(e.to));\n  return hit ? hit.at : null;\n}\n\n// \"In\u00edcio de trabalho\" robusto: 1) primeira ida para WORK; sen\u00e3o 2) primeira sa\u00edda de PREP para fora de PREP\nfunction firstWorkStart(changelog) {\n  const events = extractStatusEvents(changelog);\n  if (!events.length) return null;\n  const toWork = events.find(e => WORK_STATUSES.includes(e.to));\n  if (toWork) return toWork.at;\n  const exitPrep = events.find(e => PREP_STATUSES.includes(e.from) && !PREP_STATUSES.includes(e.to));\n  return exitPrep ? exitPrep.at : null;\n}\n\n// Conta reaberturas (saiu de Done para outro status)\nfunction countReopened(changelog) {\n  const events = extractStatusEvents(changelog);\n  let c = 0;\n  for (const e of events) {\n    if (DONE_STATUSES.includes(e.from) && !DONE_STATUSES.includes(e.to)) c++;\n  }\n  return c;\n}\n\n// Tempo total (horas) em um status espec\u00edfico (calend\u00e1rio)\nfunction totalTimeInStatusHours(changelog, statusName, now) {\n  const sNorm = norm(statusName);\n  const events = extractStatusEvents(changelog);\n  if (!events.length) return 0;\n\n  let totalMs = 0;\n  let inStatus = false;\n  let enteredAt = null;\n\n  for (const e of events) {\n    if (!inStatus && e.to === sNorm) {\n      inStatus = true; enteredAt = e.at;\n    } else if (inStatus && e.from === sNorm) {\n      totalMs += (e.at - enteredAt);\n      inStatus = false; enteredAt = null;\n    }\n  }\n  if (inStatus && enteredAt) totalMs += (now - enteredAt);\n  return totalMs / 3600000;\n}\n\n// Tempo por fase (dias \u00fateis). Soma os intervalos entre eventos por status \u2192 fase\nfunction phaseTimesBusinessDays(changelog, fields, now) {\n  const events = extractStatusEvents(changelog);\n  const out = { discovery: 0, backlog: 0, build: 0, review: 0, validation: 0, deploy: 0, done: 0 };\n\n  if (!events.length) return out;\n\n  // Tentativa de status \"inicial\": usa from do primeiro evento; se nulo, usa status atual do fields como ponto de partida\n  let curStatus = events[0].from || norm(fields?.status?.name);\n  let curAt = new Date(fields?.created || events[0].at);\n\n  for (const e of events) {\n    const phase = phaseOfStatus(curStatus);\n    const segEnd = e.at;\n    const days = businessDiffDays(curAt, segEnd);\n    if (Number.isFinite(days)) {\n      if (!out[phase]) out[phase] = 0;\n      out[phase] += days;\n    }\n    // avan\u00e7a\n    curStatus = e.to;\n    curAt = e.at;\n  }\n\n  // \u00faltimo trecho at\u00e9 \"agora\"\n  const lastPhase = phaseOfStatus(curStatus);\n  const days = businessDiffDays(curAt, now);\n  if (Number.isFinite(days)) {\n    if (!out[lastPhase]) out[lastPhase] = 0;\n    out[lastPhase] += days;\n  }\n\n  // arredonda com 2 casas\n  for (const k of Object.keys(out)) out[k] = Number(out[k].toFixed(2));\n  return out;\n}\n\n// Detecta quando a issue entrou na sprint (via altera\u00e7\u00e3o no campo Sprint)\nfunction getSprintEntryDate(changelog, sprintId) {\n  if (!changelog?.histories) return null;\n  for (const h of changelog.histories) {\n    for (const it of h.items || []) {\n      if (it.field === 'Sprint') {\n        const raw = (it.to || it.toString || '') + '';\n        if (raw.includes(String(sprintId))) return new Date(h.created);\n      }\n    }\n  }\n  return null;\n}\n\n// Fluxo bruto de status (para debug/insights)\nfunction getStatusFlow(changelog) {\n  const events = extractStatusEvents(changelog);\n  return events.map(e => ({\n    from: e.from || null,\n    to: e.to || null,\n    at: e.at.toISOString()\n  }));\n}\n\n// ============================================\n// PROCESSAMENTO\n// ============================================\n\nconst input = $json;\nconst sprintId = input.sprintId;\nconst sprintName = input.sprintName;\nconst sprintStart = input.sprintStartDate ? new Date(input.sprintStartDate) : null;\nconst sprintEnd = input.sprintEndDate ? new Date(input.sprintEndDate) : null;\nconst sprintComplete = input.sprintCompleteDate ? new Date(input.sprintCompleteDate) : sprintEnd;\nconst issues = Array.isArray(input.issues) ? input.issues : [];\n\nconst now = new Date();\n\nconst bugTypes = ['bug','defeito'].map(norm);\nconst blockedStatusName = 'Blocked'; // ajuste se usa outro r\u00f3tulo\n\n// ===== Enriquecimento por issue =====\nconst enrichedIssues = issues.map(issue => {\n  const fields = issue.fields || {};\n  const changelog = issue.changelog || {};\n\n  // Datas b\u00e1sicas\n  const created = fields.created ? new Date(fields.created) : null;\n  const resolved = fields.resolutiondate ? new Date(fields.resolutiondate) : null;\n  const updated = fields.updated ? new Date(fields.updated) : null;\n\n  // Status / tipo\n  const statusName = fields.status?.name || null;\n  const statusNorm = norm(statusName);\n  const statusCategory = fields.status?.statusCategory?.name || null;\n  const isDone = DONE_STATUSES.includes(statusNorm) || norm(statusCategory) === 'done';\n  const issueType = fields.issuetype?.name || 'Unknown';\n  const isBug = bugTypes.includes(norm(issueType));\n  const priority = fields.priority?.name || 'None';\n\n  // Pessoas\n  const assignee = fields.assignee?.displayName || 'Unassigned';\n  const reporter = fields.reporter?.displayName || 'Unknown';\n\n  // Transi\u00e7\u00f5es chave\n  const firstDone = firstTransitionToNormalized(changelog, DONE_STATUSES);\n  const workStartAt = firstWorkStart(changelog);\n\n  // M\u00e9tricas de tempo (DIAS \u00daTEIS)\n  const leadTimeDays  = (created && resolved) ? businessDiffDays(created, resolved) : null;\n  const cycleTimeDays = (workStartAt && resolved) ? businessDiffDays(workStartAt, resolved) : null;\n  const waitTimeDays  = (created && workStartAt) ? businessDiffDays(created, workStartAt) : null;\n\n  // Aging (abertas): desde \u00faltima mudan\u00e7a at\u00e9 agora, em DIAS \u00daTEIS\n  let lastChangeDate = updated || created;\n  if (changelog?.histories?.length > 0) {\n    const lastHistory = changelog.histories[changelog.histories.length - 1];\n    const lastHistoryDate = new Date(lastHistory.created);\n    if (lastHistoryDate > lastChangeDate) lastChangeDate = lastHistoryDate;\n  }\n  const agingDays = !isDone && lastChangeDate ? businessDiffDays(lastChangeDate, now) : 0;\n\n  // Sprint\n  const enteredSprintAt = getSprintEntryDate(changelog, sprintId);\n  const isLateEntry = !!(enteredSprintAt && sprintStart && (enteredSprintAt > sprintStart));\n  const wasInSprintAtStart = (!isLateEntry) && (!!sprintStart) && ((created && created < sprintStart) || (enteredSprintAt && enteredSprintAt < sprintStart));\n\n  // Qualidade\n  const reopenedCount = countReopened(changelog);\n  const hasRework = reopenedCount > 0;\n  const blockedTimeHours = blockedStatusName ? totalTimeInStatusHours(changelog, blockedStatusName, now) : 0;\n\n  // Fluxo\n  const statusFlow = getStatusFlow(changelog);\n  const statusChanges = statusFlow.length;\n\n  // Descri\u00e7\u00e3o\n  const description = adfToText(fields.description);\n  const hasDescription = description.length > 10;\n\n  // Subtasks\n  const hasSubtasks = Array.isArray(fields.subtasks) && fields.subtasks.length > 0;\n  const subtasksCount = hasSubtasks ? fields.subtasks.length : 0;\n\n  // Labels\n  const labels = Array.isArray(fields.labels) ? fields.labels : [];\n\n  // Tempos por fase (dias \u00fateis)\n  const phaseTimesDays = phaseTimesBusinessDays(changelog, fields, now);\n\n  // Primeira \"ida para trabalho ativo\" (para debug/insights)\n  const firstInProgress = firstTransitionToNormalized(changelog, WORK_STATUSES);\n\n  return {\n    key: issue.key,\n    summary: fields.summary || '',\n    description_text: description,\n    has_description: hasDescription,\n\n    status_current: statusName,\n    status_category: statusCategory,\n    is_done: isDone,\n    issue_type: issueType,\n    is_bug: isBug,\n    priority: priority,\n\n    assignee,\n    reporter,\n\n    created: created?.toISOString() || null,\n    resolved: resolved?.toISOString() || null,\n    updated: updated?.toISOString() || null,\n    first_in_progress: firstInProgress?.toISOString() || null,\n    first_done: firstDone?.toISOString() || null,\n\n    // tempos (DIAS \u00daTEIS)\n    lead_time_days: leadTimeDays !== null ? Number(leadTimeDays.toFixed(2)) : null,\n    cycle_time_days: cycleTimeDays !== null ? Number(cycleTimeDays.toFixed(2)) : null,\n    wait_time_days:  waitTimeDays  !== null ? Number(waitTimeDays.toFixed(2))  : null,\n    aging_days: Number((agingDays || 0).toFixed(2)),\n\n    // Sprint\n    entered_sprint_at: enteredSprintAt?.toISOString() || null,\n    is_late_entry: isLateEntry,\n    was_in_sprint_at_start: !!wasInSprintAtStart,\n\n    // Qualidade\n    reopened_count: reopenedCount,\n    has_rework: hasRework,\n    blocked_time_hours: blockedTimeHours ? Number(blockedTimeHours.toFixed(2)) : 0,\n\n    // Fluxo\n    status_changes: statusChanges,\n    status_flow: statusFlow,\n\n    // Outros\n    has_subtasks: hasSubtasks,\n    subtasks_count: subtasksCount,\n    labels,\n\n    // Fases\n    phase_times_days: {\n      discovery: phaseTimesDays.discovery || 0,\n      backlog: phaseTimesDays.backlog || 0,\n      build: phaseTimesDays.build || 0,\n      review: phaseTimesDays.review || 0,\n      validation: phaseTimesDays.validation || 0,\n      deploy: phaseTimesDays.deploy || 0\n    }\n  };\n});\n\n// ============================================\n// M\u00c9TRICAS AGREGADAS\n// ============================================\n\nconst doneIssues = enrichedIssues.filter(i => i.is_done);\nconst openIssues = enrichedIssues.filter(i => !i.is_done);\nconst bugsTotal = enrichedIssues.filter(i => i.is_bug);\nconst bugsDone = doneIssues.filter(i => i.is_bug);\nconst bugsOpen = openIssues.filter(i => i.is_bug);\n\n// Escopo\nconst scopePlanned = enrichedIssues.filter(i => i.was_in_sprint_at_start).length;\nconst scopeAdded = enrichedIssues.filter(i => i.is_late_entry).length;\nconst scopeCompleted = doneIssues.length;\nconst scopeIncomplete = openIssues.length;\nconst commitmentDeliveryRatio = scopePlanned > 0 ? (scopeCompleted / scopePlanned) : null;\nconst scopeCreepRatio = enrichedIssues.length > 0 ? (scopeAdded / enrichedIssues.length) : 0;\n\n// Fluxo (dias \u00fateis)\nconst leadTimeStats = stats(doneIssues.map(i => i.lead_time_days));\nconst cycleTimeStats = stats(doneIssues.map(i => i.cycle_time_days));\nconst waitTimeStats = stats(doneIssues.map(i => i.wait_time_days));\n\n// WIP e aging (dias \u00fateis)\nconst wipAgingStats = stats(openIssues.map(i => i.aging_days));\nconst currentWip = openIssues.length;\n\n// Throughput e dura\u00e7\u00e3o de sprint (calend\u00e1rio)\nconst sprintDurationDays = (sprintStart && sprintComplete) ? (sprintComplete - sprintStart) / 86400000 : null;\nconst throughput = doneIssues.length;\nconst throughputPerDay = sprintDurationDays > 0 ? (throughput / sprintDurationDays) : null;\n\n// Qualidade\nconst reworkIssues = enrichedIssues.filter(i => i.has_rework).length;\nconst reworkRatio = doneIssues.length > 0 ? (reworkIssues / doneIssues.length) : 0;\nconst bugRatio = enrichedIssues.length > 0 ? (bugsTotal.length / enrichedIssues.length) : 0;\nconst blockedTotalHours = enrichedIssues.reduce((sum, i) => sum + (i.blocked_time_hours || 0), 0);\nconst issuesWithoutDescription = enrichedIssues.filter(i => !i.has_description).length;\n\n// Balance por pessoa\nconst loadBalance = {};\nfor (const it of enrichedIssues) {\n  const who = it.assignee || 'Unassigned';\n  if (!loadBalance[who]) loadBalance[who] = { total: 0, done: 0, open: 0, bugs: 0 };\n  loadBalance[who].total++;\n  if (it.is_done) loadBalance[who].done++; else loadBalance[who].open++;\n  if (it.is_bug) loadBalance[who].bugs++;\n}\n\n// Distribui\u00e7\u00e3o por tipo / prioridade\nconst issueTypesDist = {};\nfor (const it of enrichedIssues) {\n  const t = it.issue_type || 'Unknown';\n  if (!issueTypesDist[t]) issueTypesDist[t] = { total: 0, done: 0 };\n  issueTypesDist[t].total++;\n  if (it.is_done) issueTypesDist[t].done++;\n}\nconst priorityDist = {};\nfor (const it of enrichedIssues) {\n  const p = it.priority || 'None';\n  if (!priorityDist[p]) priorityDist[p] = { total: 0, done: 0 };\n  priorityDist[p].total++;\n  if (it.is_done) priorityDist[p].done++;\n}\n\n// Top aging / status changes\nconst topAgingIssues = [...openIssues]\n  .sort((a,b) => (b.aging_days||0) - (a.aging_days||0))\n  .slice(0, 10)\n  .map(i => ({\n    key: i.key, summary: i.summary, assignee: i.assignee, status: i.status_current,\n    aging_days: i.aging_days, priority: i.priority, issue_type: i.issue_type\n  }));\n\nconst topStatusChanges = [...enrichedIssues]\n  .sort((a,b) => (b.status_changes||0) - (a.status_changes||0))\n  .slice(0, 10)\n  .map(i => ({\n    key: i.key, summary: i.summary, status_changes: i.status_changes,\n    is_done: i.is_done, reopened_count: i.reopened_count\n  }));\n\n// Previsibilidade (coef. varia\u00e7\u00e3o do cycle time em DIAS \u00daTEIS)\nconst cycleAvg = cycleTimeStats.avg;\nlet cycleCV = null, predictabilityScore = null;\nif (cycleAvg && cycleAvg > 0 && doneIssues.length > 1) {\n  const variance = doneIssues.reduce((acc, i) => {\n    if (!Number.isFinite(i.cycle_time_days)) return acc;\n    const d = i.cycle_time_days - cycleAvg;\n    return acc + d*d;\n  }, 0) / (doneIssues.length - 1);\n  const stdev = Math.sqrt(variance);\n  cycleCV = stdev / cycleAvg;\n  predictabilityScore = Math.max(0, Math.min(100, 100 * (1 - cycleCV)));\n}\n\n// ============================================\n// RESULTADO\n// ============================================\n\nreturn [{\n  json: {\n    sprint: {\n      id: sprintId,\n      name: sprintName,\n      start_date: sprintStart?.toISOString() || null,\n      end_date: sprintEnd?.toISOString() || null,\n      complete_date: sprintComplete?.toISOString() || null,\n      duration_days: sprintDurationDays ? Number(sprintDurationDays.toFixed(1)) : null,\n      goal: input.sprintGoal || ''\n    },\n    metrics: {\n      scope: {\n        total: enrichedIssues.length,\n        planned: scopePlanned,\n        added: scopeAdded,\n        completed: scopeCompleted,\n        incomplete: scopeIncomplete,\n        commitment_delivery_ratio: commitmentDeliveryRatio ? Number(commitmentDeliveryRatio.toFixed(2)) : null,\n        scope_creep_ratio: Number(scopeCreepRatio.toFixed(2))\n      },\n      flow: {\n        throughput: throughput,\n        throughput_per_day: throughputPerDay ? Number(throughputPerDay.toFixed(2)) : null,\n        // DIAS \u00daTEIS\n        lead_time: {\n          p50: leadTimeStats.p50 ? Number(leadTimeStats.p50.toFixed(2)) : null,\n          p90: leadTimeStats.p90 ? Number(leadTimeStats.p90.toFixed(2)) : null,\n          p95: leadTimeStats.p95 ? Number(leadTimeStats.p95.toFixed(2)) : null,\n          avg: leadTimeStats.avg ? Number(leadTimeStats.avg.toFixed(2)) : null,\n          min: leadTimeStats.min ? Number(leadTimeStats.min.toFixed(2)) : null,\n          max: leadTimeStats.max ? Number(leadTimeStats.max.toFixed(2)) : null\n        },\n        cycle_time: {\n          p50: cycleTimeStats.p50 ? Number(cycleTimeStats.p50.toFixed(2)) : null,\n          p90: cycleTimeStats.p90 ? Number(cycleTimeStats.p90.toFixed(2)) : null,\n          p95: cycleTimeStats.p95 ? Number(cycleTimeStats.p95.toFixed(2)) : null,\n          avg: cycleTimeStats.avg ? Number(cycleTimeStats.avg.toFixed(2)) : null,\n          min: cycleTimeStats.min ? Number(cycleTimeStats.min.toFixed(2)) : null,\n          max: cycleTimeStats.max ? Number(cycleTimeStats.max.toFixed(2)) : null\n        },\n        wait_time: {\n          p50: waitTimeStats.p50 ? Number(waitTimeStats.p50.toFixed(2)) : null,\n          p90: waitTimeStats.p90 ? Number(waitTimeStats.p90.toFixed(2)) : null,\n          avg: waitTimeStats.avg ? Number(waitTimeStats.avg.toFixed(2)) : null\n        }\n      },\n      wip: {\n        current: currentWip,\n        aging_p50: wipAgingStats.p50 ? Number(wipAgingStats.p50.toFixed(2)) : null,\n        aging_p90: wipAgingStats.p90 ? Number(wipAgingStats.p90.toFixed(2)) : null,\n        aging_max: wipAgingStats.max ? Number(wipAgingStats.max.toFixed(2)) : null,\n        by_phase: {\n          discovery: Number(openIssues.reduce((s,i)=>s+(i.phase_times_days?.discovery||0),0).toFixed(0)),\n          backlog:   Number(openIssues.reduce((s,i)=>s+(i.phase_times_days?.backlog||0),0).toFixed(0)),\n          build:     Number(openIssues.reduce((s,i)=>s+(i.phase_times_days?.build||0),0).toFixed(0)),\n          review:    Number(openIssues.reduce((s,i)=>s+(i.phase_times_days?.review||0),0).toFixed(0)),\n          validation:Number(openIssues.reduce((s,i)=>s+(i.phase_times_days?.validation||0),0).toFixed(0)),\n          deploy:    Number(openIssues.reduce((s,i)=>s+(i.phase_times_days?.deploy||0),0).toFixed(0)),\n        }\n      },\n      quality: {\n        bugs_total: bugsTotal.length,\n        bugs_done: bugsDone.length,\n        bugs_open: bugsOpen.length,\n        bug_ratio: Number(bugRatio.toFixed(2)),\n        rework_issues: reworkIssues,\n        rework_ratio: Number(reworkRatio.toFixed(2)),\n        blocked_total_hours: Number(blockedTotalHours.toFixed(2)),\n        issues_without_description: issuesWithoutDescription\n      },\n      predictability: {\n        score: predictabilityScore !== null ? Number(predictabilityScore.toFixed(1)) : null,\n        coefficient_variation: cycleCV !== null ? Number(cycleCV.toFixed(2)) : null,\n        note: \"Score 0-100: quanto maior, mais previs\u00edvel (cycle time em dias \u00fateis)\"\n      },\n      distributions: {\n        by_assignee: loadBalance,\n        by_issue_type: issueTypesDist,\n        by_priority: priorityDist\n      }\n    },\n    insights: {\n      top_aging_issues: topAgingIssues,\n      top_status_changes: topStatusChanges\n    },\n    issues_detail: enrichedIssues\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1584,
        -64
      ],
      "id": "d16f1091-d1ef-4193-bfe0-e2fa23c3591b",
      "name": "7. Calcular M\u00e9tricas"
    },
    {
      "parameters": {
        "modelId": {
          "__rl": true,
          "value": "models/gemini-2.5-pro",
          "mode": "list",
          "cachedResultName": "models/gemini-2.5-pro"
        },
        "messages": {
          "values": [
            {
              "content": "=Voc\u00ea \u00e9 um gerente de engenharia experiente em m\u00e9tricas de fluxo e Kanban.\n\nAnalise os dados desta sprint e gere um resumo executivo t\u00e9cnico, com foco em **sa\u00fade do fluxo de trabalho** e **efici\u00eancia da entrega**.\n\nUse as seguintes refer\u00eancias como base de compara\u00e7\u00e3o (em dias \u00fateis ou propor\u00e7\u00f5es):\n- Cycle time (p90) ideal: \u2264 7 dias\n- Lead time (p90) ideal: \u2264 10 dias\n- Aging: deve ser menor ou igual ao p90 do cycle time\n- Rework: ideal \u2264 5%\n- Bugs: ideal \u2264 10%\n- WIP: ideal \u2264 1,5x o throughput m\u00e9dio da sprint\n\n**N\u00e3o enfatize scope creep ou compromisso vs entrega** \u2014 o fluxo \u00e9 puxado e prioriza estabilidade, n\u00e3o cumprimento de escopo planejado.\n\n**Instru\u00e7\u00f5es:**\n- Seja direto e t\u00e9cnico, sem floreios\n- Use valores do JSON sempre que poss\u00edvel\n- Considere apenas dias \u00fateis (ignore fins de semana) nos c\u00e1lculos de tempo\n- SEMPRE que mencionar uma issue (DH-XXX), inclua o summary: \"DH-XXX: T\u00edtulo\"\n- **AGING/ESPERA**: S\u00f3 considere problema se o item estiver em trabalho ativo ('Em andamento', 'Code Review', 'Valida\u00e7\u00e3o', 'Aceita\u00e7\u00e3o', 'Pronto para Deploy'). IGNORE aging/espera de itens em prepara\u00e7\u00e3o ('Tarefas Pendentes', 'Priorizado', 'Refinamento T\u00e9cnico', 'Design Review', etc.) - \u00e9 NORMAL que fiquem esperando.\n- **DISTRIBUI\u00c7\u00c3O**: O time tem especializa\u00e7\u00e3o t\u00e9cnica. N\u00c3O mencione \"capacidade ociosa\" ou sugira \"puxar novo item\" para pessoas com poucos itens. Distribui\u00e7\u00e3o desigual \u00e9 NORMAL devido a skills diferentes.\n- **IMPORTANTE: Inclua TODAS as pessoas em per_participant** \u2014 use metrics.distributions.by_assignee. N\u00e3o omita ningu\u00e9m.\n- Retorne SOMENTE JSON v\u00e1lido\n- Copie sprint_name e sprint_id do input\n\n**Formato de sa\u00edda:**\n{\n  \"sprint_name\": \"{{ $json.sprint.name }}\",\n  \"sprint_id\": \"{{ $json.sprint.id }}\",\n  \"executive_summary_html\": \"1-2 par\u00e1grafos em HTML com panorama geral da fluidez, gargalos e estabilidade do sistema\",\n  \"metrics_summary\": [\n    {\n      \"metric\": \"Cycle Time (p90)\",\n      \"value\": \"X dias\",\n      \"status\": \"ideal | aceit\u00e1vel | alerta\",\n      \"comment\": \"breve explica\u00e7\u00e3o\"\n    }\n  ],\n  \"key_insights\": [\n    {\n      \"title\": \"t\u00edtulo curto\",\n      \"description\": \"observa\u00e7\u00e3o pr\u00e1tica sobre aging/espera de itens em TRABALHO ATIVO, balanceamento e foco. N\u00c3O mencione espera/aging de itens em prepara\u00e7\u00e3o.\",\n      \"action\": \"a\u00e7\u00e3o recomendada\",\n      \"priority\": \"alta | m\u00e9dia | baixa\"\n    }\n  ],\n  \"wip_health\": {\n    \"current\": \"n\u00famero\",\n    \"aging_hotspots\": [\n      {\"key\":\"DH-XXX\",\"summary\":\"t\u00edtulo\",\"assignee\":\"...\",\"status\":\"...\",\"aging_days\":0}\n    ]\n  },\n  \"per_participant\": [\n    {\"person\":\"nome (TODAS as pessoas de metrics.distributions.by_assignee)\",\"observation\":\"breve observa\u00e7\u00e3o sobre balanceamento de carga. N\u00c3O mencione capacidade ociosa - o time tem especializa\u00e7\u00e3o.\",\"suggestion\":\"sugest\u00e3o focada em qualidade/efici\u00eancia, n\u00e3o em volume\"},\n    {\"person\":\"Unassigned\",\"observation\":\"SOMENTE mencione se houver itens Unassigned nos seguintes status: 'Em andamento', 'Code Review', 'Valida\u00e7\u00e3o', 'Aceita\u00e7\u00e3o', 'Pronto para Deploy'. Ignore completamente itens Unassigned em status de prepara\u00e7\u00e3o/discovery como: 'Tarefas Pendentes', 'Priorizado', 'Refinamento T\u00e9cnico', 'Design Review', 'Ideias e Insights', 'Explora\u00e7\u00e3o e An\u00e1lise', 'Prototipagem e Valida\u00e7\u00e3o', 'To Do' - nesses casos \u00e9 ESPERADO que n\u00e3o tenham assignee.\",\"suggestion\":\"atribuir respons\u00e1vel para itens em trabalho ativo\"}\n  ],\n  \"rationale\": \"1-2 frases explicando o porqu\u00ea das tend\u00eancias\"\n}\n\n**Dados da sprint:**\n{{ JSON.stringify($json) }}"
            }
          ]
        },
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.googleGemini",
      "typeVersion": 1,
      "position": [
        1792,
        -64
      ],
      "id": "0e5f0155-7a1f-4d4d-a40d-3552d0eb8d0b",
      "name": "Message a model",
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Extrai o JSON do Gemini (que vem dentro de markdown code block)\nconst rawGemini = $json;\n\nlet geminiResponse;\ntry {\n  let textContent = rawGemini.content?.parts?.[0]?.text || JSON.stringify(rawGemini);\n  \n  // Remove markdown code blocks\n  textContent = textContent.trim();\n  if (textContent.startsWith('```json')) {\n    textContent = textContent.replace(/^```json\\s*/g, '').replace(/```\\s*$/g, '');\n  } else if (textContent.startsWith('```')) {\n    textContent = textContent.replace(/^```\\s*/g, '').replace(/```\\s*$/g, '');\n  }\n  \n  geminiResponse = JSON.parse(textContent);\n  \n} catch (error) {\n  throw new Error(`Erro ao processar Gemini: ${error.message}`);\n}\n\nconst sprintName = geminiResponse.sprint_name || 'Sprint Review';\nconst sprintId = geminiResponse.sprint_id || '';\n\n// Gera HTML para Confluence (formato Storage)\nfunction generateConfluenceHTML(data) {\n  let html = '<ac:structured-macro ac:name=\"info\">';\n  html += '<ac:rich-text-body>';\n  html += `<p><strong>Sprint:</strong> ${data.sprint_name || sprintName} (ID: ${data.sprint_id || sprintId})</p>`;\n  html += '</ac:rich-text-body>';\n  html += '</ac:structured-macro>';\n  \n  // Resumo Executivo\n  html += '<h2>\ud83d\udcca Resumo Executivo</h2>';\n  html += data.executive_summary_html || '<p>Sem resumo dispon\u00edvel.</p>';\n  \n  // Tabela de M\u00e9tricas (NOVO FORMATO)\n  if (data.metrics_summary && data.metrics_summary.length > 0) {\n    html += '<h2>\ud83d\udcc8 M\u00e9tricas</h2>';\n    html += '<table><tbody>';\n    html += '<tr><th>M\u00e9trica</th><th>Valor</th><th>Status</th><th>Coment\u00e1rio</th></tr>';\n    for (const m of data.metrics_summary) {\n      const statusIcon = m.status === 'ideal' ? '\ud83d\udfe2' : (m.status === 'aceit\u00e1vel' ? '\ud83d\udfe1' : '\ud83d\udd34');\n      html += `<tr>`;\n      html += `<td>${m.metric}</td>`;\n      html += `<td><strong>${m.value}</strong></td>`;\n      html += `<td>${statusIcon} ${m.status}</td>`;\n      html += `<td>${m.comment || ''}</td>`;\n      html += `</tr>`;\n    }\n    html += '</tbody></table>';\n  }\n  \n  // WIP Health\n  if (data.wip_health) {\n    html += '<h2>\ud83d\udd04 Sa\u00fade do WIP</h2>';\n    html += `<p><strong>WIP Atual:</strong> ${data.wip_health.current || 0}</p>`;\n    \n    if (data.wip_health.aging_hotspots && data.wip_health.aging_hotspots.length > 0) {\n      html += '<h3>\u26a0\ufe0f Hotspots de Aging</h3>';\n      html += '<table><tbody>';\n      html += '<tr><th>Issue</th><th>Assignee</th><th>Status</th><th>Aging (dias)</th></tr>';\n      for (const item of data.wip_health.aging_hotspots) {\n        const issueDisplay = item.summary ? `${item.key}: ${item.summary}` : item.key;\n        html += `<tr><td>${issueDisplay}</td><td>${item.assignee}</td><td>${item.status}</td><td>${item.aging_days}</td></tr>`;\n      }\n      html += '</tbody></table>';\n    }\n  }\n  \n  // Key Insights (NOVO FORMATO)\n  if (data.key_insights && data.key_insights.length > 0) {\n    html += '<h2>\ud83d\udca1 Insights Acion\u00e1veis</h2>';\n    for (const insight of data.key_insights) {\n      const priorityIcon = insight.priority === 'alta' ? '\ud83d\udd34' : (insight.priority === 'm\u00e9dia' ? '\ud83d\udfe1' : '\ud83d\udfe2');\n      const title = insight.title || insight.theme || 'Insight';\n      html += '<ac:structured-macro ac:name=\"expand\">';\n      html += `<ac:parameter ac:name=\"title\">${priorityIcon} ${title}</ac:parameter>`;\n      html += '<ac:rich-text-body>';\n      html += `<p><strong>Descri\u00e7\u00e3o:</strong> ${insight.description || insight.evidence || 'N/A'}</p>`;\n      html += `<p><strong>A\u00e7\u00e3o recomendada:</strong> ${insight.action || 'N/A'}</p>`;\n      html += '</ac:rich-text-body>';\n      html += '</ac:structured-macro>';\n    }\n  }\n  \n  // Por Participante (NOVO FORMATO)\n  if (data.per_participant && data.per_participant.length > 0) {\n    html += '<h2>\ud83d\udc65 Por Participante</h2>';\n    html += '<table><tbody>';\n    html += '<tr><th>Pessoa</th><th>Observa\u00e7\u00e3o</th><th>Sugest\u00e3o</th></tr>';\n    for (const p of data.per_participant) {\n      html += `<tr><td>${p.person}</td><td>${p.observation || p.tone || ''}</td><td>${p.suggestion || p.suggested_next_step || ''}</td></tr>`;\n    }\n    html += '</tbody></table>';\n  }\n  \n  // Rationale\n  if (data.rationale) {\n    html += '<h2>\ud83c\udfaf An\u00e1lise Geral</h2>';\n    html += `<p>${data.rationale}</p>`;\n  }\n  \n  return html;\n}\n\n// Gera o t\u00edtulo da p\u00e1gina usando o nome REAL da sprint\nconst pageTitle = `Sprint Insights: ${sprintName}`;\n\n// Gera o HTML\nconst htmlBody = generateConfluenceHTML(geminiResponse);\n\nreturn [{\n  json: {\n    pageTitle: pageTitle,\n    spaceKey: 'DHub',\n    htmlBody: htmlBody,\n    geminiData: geminiResponse\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2000,
        -64
      ],
      "id": "f1a2b3c4-d5e6-7890-abcd-ef1234567891",
      "name": "9. Formatar para Confluence"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://gupy-io.atlassian.net/wiki/rest/api/content",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ {\n  \"type\": \"page\",\n  \"title\": $json.pageTitle,\n  \"space\": {\n    \"key\": $json.spaceKey\n  },\n  \"ancestors\": [\n    {\n      \"id\": \"4709056599\"\n    }\n  ],\n  \"body\": {\n    \"storage\": {\n      \"value\": $json.htmlBody,\n      \"representation\": \"storage\"\n    }\n  }\n} }}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2208,
        -64
      ],
      "id": "a9b8c7d6-e5f4-3210-abcd-ef9876543210",
      "name": "10. Publicar no Confluence",
      "credentials": {
        "httpBasicAuth": {
          "name": "<your credential>"
        }
      }
    }
  ],
  "connections": {
    "When clicking 'Execute workflow'": {
      "main": [
        [
          {
            "node": "1. Buscar Sprints Fechadas",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "1. Buscar Sprints Fechadas": {
      "main": [
        [
          {
            "node": "2. Selecionar Sprint",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "2. Selecionar Sprint": {
      "main": [
        [
          {
            "node": "3. Buscar Issues da Sprint (Jira)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "3. Buscar Issues da Sprint (Jira)": {
      "main": [
        [
          {
            "node": "4. Preparar Lista de Issues",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "4. Preparar Lista de Issues": {
      "main": [
        [
          {
            "node": "5. Buscar Detalhes Completos",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "5. Buscar Detalhes Completos": {
      "main": [
        [
          {
            "node": "6. Consolidar Issues",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "6. Consolidar Issues": {
      "main": [
        [
          {
            "node": "7. Calcular M\u00e9tricas",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "7. Calcular M\u00e9tricas": {
      "main": [
        [
          {
            "node": "Message a model",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Message a model": {
      "main": [
        [
          {
            "node": "9. Formatar para Confluence",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "9. Formatar para Confluence": {
      "main": [
        [
          {
            "node": "10. Publicar no Confluence",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "bddf7e29-1138-41cd-9090-dd253ae6e31d",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "id": "Iyfe8eBToZCNxkft",
  "tags": []
}