{
  "name": "LogSentinel Workflow",
  "nodes": [
    {
      "parameters": {
        "schema": {
          "__rl": true,
          "mode": "list",
          "value": "public"
        },
        "table": {
          "__rl": true,
          "value": "logs",
          "mode": "list",
          "cachedResultName": "logs"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [
            "id"
          ],
          "schema": [
            {
              "id": "id",
              "displayName": "id",
              "required": false,
              "defaultMatch": true,
              "display": true,
              "type": "number",
              "canBeUsedToMatch": true,
              "removed": true
            },
            {
              "id": "source_id",
              "displayName": "source_id",
              "required": true,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "log_timestamp",
              "displayName": "log_timestamp",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "dateTime",
              "canBeUsedToMatch": true
            },
            {
              "id": "level",
              "displayName": "level",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": true
            },
            {
              "id": "category",
              "displayName": "category",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": true
            },
            {
              "id": "message",
              "displayName": "message",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "raw_log",
              "displayName": "raw_log",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "format_type",
              "displayName": "format_type",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "afd_state",
              "displayName": "afd_state",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": true
            },
            {
              "id": "afd_symbol",
              "displayName": "afd_symbol",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": true
            },
            {
              "id": "previous_state",
              "displayName": "previous_state",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": true
            },
            {
              "id": "llm_cache_hit",
              "displayName": "llm_cache_hit",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "boolean",
              "canBeUsedToMatch": true,
              "removed": true
            },
            {
              "id": "classified_at",
              "displayName": "classified_at",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "dateTime",
              "canBeUsedToMatch": true,
              "removed": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        -432,
        664
      ],
      "id": "020bf0b0-5c3b-4b74-b865-a34756319d25",
      "name": "Insert rows in a table",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const body = $json.body || $json;\n\nlet raw_log;\nlet source_id;\n\n// Si body est une string : Content-Type text/plain \u2192 le body EST le log\nif (typeof body === \"string\") {\n  raw_log   = body;\n  source_id = \"unknown\";\n} else {\n  // Content-Type application/json \u2192 body a les champs raw_log et source_id\n  raw_log   = body.raw_log   ?? null;\n  source_id = body.source_id || \"unknown\";\n}\n\nfunction detectFormat(log) {\n  if (!log || typeof log !== \"string\" || log.trim() === \"\") return \"unknown\";\n\n  // JSON : commence par {\n  if (log.trimStart().startsWith(\"{\")) return \"json\";\n\n  // Apache : commence par une IP suivie de \"METHOD\n  if (/^\\d+\\.\\d+\\.\\d+\\.\\d+.*\"(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)/.test(log)) return \"apache\";\n\n  // Syslog : Mois JJ HH:MM:SS hostname process[pid]:\n  if (/^[A-Z][a-z]{2}\\s+\\d{1,2}\\s+\\d{2}:\\d{2}:\\d{2}\\s+\\S+\\s+\\S+\\[/.test(log)) return \"syslog\";\n\n  // Plaintext : commence par une date YYYY-MM-DD\n  if (/^\\d{4}-\\d{2}-\\d{2}/.test(log)) return \"plaintext\";\n\n  return \"unknown\";\n}\n\nreturn {\n  raw_log,\n  source_id,\n  format_type: detectFormat(raw_log)\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -1104,
        760
      ],
      "id": "7bf2bb18-02b4-4600-922a-54aecbc9029a",
      "name": "Format Detector"
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "leftValue": "={{$json.format_type}}",
                    "rightValue": "json",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "id": "a567dbb2-09e4-4a0f-bb09-68436f7dd491"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "JSON Processor"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "id": "b10f9faa-df5a-4c60-81bc-8e9527ea522d",
                    "leftValue": "={{$json.format_type}}",
                    "rightValue": "syslog",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "Syslog Processor"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "id": "591c9d88-29f2-4a9b-b9e5-fc53137fac20",
                    "leftValue": "={{$json.format_type}}",
                    "rightValue": "apache",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "Apache Processor"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "id": "7359ff2b-2af0-4189-9e0f-b713fa7eec3a",
                    "leftValue": "={{$json.format_type}}",
                    "rightValue": "plaintext",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "Plaintext Processor"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "id": "0a645b9e-9940-4670-b75d-08b82fbf443d",
                    "leftValue": "={{$json.format_type}}",
                    "rightValue": "unknown",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "Unknown Processor"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.4,
      "position": [
        -880,
        712
      ],
      "id": "e2012df9-a25a-4a96-a2d6-f0b28bccd033",
      "name": "Switch"
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const rawStr = $json.raw_log;\nconst bodySourceId = $json.source_id || \"unknown\";\n\nlet log = {};\ntry {\n  log = typeof rawStr === \"string\" ? JSON.parse(rawStr) : {};\n} catch(e) { log = {}; }\n\n// Pr\u00e9fixer le message avec le level pour que les regex le capturent\nconst level   = log.level || \"\";\nconst message = log.message || JSON.stringify(log);\nconst fullMessage = level ? `${level}: ${message}` : message;\n\nreturn {\n  source_id:     log.service || log.source_id || bodySourceId,\n  log_timestamp: log.timestamp || new Date().toISOString(),\n  raw_log:       rawStr,\n  message:       fullMessage,\n  format_type:   \"json\"\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -656,
        256
      ],
      "id": "b60dd655-01b5-4924-857d-8eb917e9868f",
      "name": "JSON Processor"
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const log = $json.raw_log;\n\nconst match = typeof log === \"string\"\n  ? log.match(/^[A-Z][a-z]{2}\\s+\\d+\\s+\\d+:\\d+:\\d+\\s+\\S+\\s+(\\S+)\\[\\d+\\]:\\s+(.*)$/)\n  : null;\n\nreturn {\n  source_id: match?.[1] || $json.source_id || \"unknown\",\n  log_timestamp: new Date().toISOString(),\n  raw_log: log,\n  message: match?.[2] || log,\n  format_type: \"syslog\"\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -656,
        448
      ],
      "id": "b96933ef-fee4-4469-8160-d866e6c8638b",
      "name": "Syslog Processor"
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const log = $json.raw_log;\n\nconst match = typeof log === \"string\"\n  ? log.match(/(\\d+\\.\\d+\\.\\d+\\.\\d+).*?\\[(.*?)\\]\\s+\"(.*?)\"\\s+\\d+\\s+\\d+(.*)/)\n  : null;\n\n// match[3] = la requ\u00eate HTTP  (\"POST /api/checkout\")\n// match[4] = tout ce qui suit (CRITICAL: Database connection pool exhausted)\nconst requestPart  = match?.[3] || \"\";\nconst trailingPart = match?.[4]?.trim() || \"\";\n\nreturn {\n  source_id:     $json.source_id || \"apache\",\n  log_timestamp: match?.[2] || new Date().toISOString(),\n  raw_log:       log,\n  message:       trailingPart\n                   ? `${requestPart} ${trailingPart}`\n                   : (requestPart || log),\n  format_type:   \"apache\"\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -656,
        640
      ],
      "id": "c901f10c-306b-4e76-a3ae-d67ff8c9c0a2",
      "name": "Apache Processor"
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const log = $json.raw_log;\n\nconst parts = typeof log === \"string\" ? log.split(\" \") : [];\n\nreturn {\n  source_id: $json.source_id || \"unknown\",\n  log_timestamp: parts.slice(0, 2).join(\" \") || new Date().toISOString(),\n  raw_log: log,\n  message: parts.slice(2).join(\" \") || log,\n  format_type: \"text\"\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -656,
        832
      ],
      "id": "642fb2ab-663c-4cb5-9dfe-8d73a1a682e5",
      "name": "Plaintext Processor"
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "return {\n  source_id: $json.source_id || \"unknown\",\n  log_timestamp: new Date().toISOString(),\n  raw_log: $json.raw_log,\n  message: $json.raw_log,\n  format_type: \"unknown\"\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -656,
        1024
      ],
      "id": "5c8815df-149b-4656-a728-18338e4457a3",
      "name": "Unknown Processor"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT\n  COALESCE(s.current_state, 'INFO')   AS current_state,\n  COALESCE(s.cascade_count, 0)        AS cascade_count,\n  s.last_event_time IS NULL           AS is_new_service,\n  COALESCE(\n    EXTRACT(EPOCH FROM (NOW() - s.last_event_time))::int,\n    999999\n  )                                   AS elapsed_since_last_sec,\n  COALESCE(\n    EXTRACT(EPOCH FROM (NOW() - s.cascade_start_time))::int,\n    999999\n  )                                   AS cascade_elapsed_sec\nFROM (SELECT 1) AS dummy\nLEFT JOIN afd_states s\n  ON s.source_id = '{{ $json.source_id }}'\n AND '{{ $json.source_id }}' <> ''",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        -208,
        664
      ],
      "id": "5660c632-b83c-49e8-aa3f-f2acbaaf0aea",
      "name": "AFD State Read",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// 0. CONFIG\nconst TIMEOUT_WINDOW_SEC  = parseInt($env.TIMEOUT_WINDOW    ?? '300', 10);\nconst CASCADE_THRESHOLD   = parseInt($env.CASCADE_THRESHOLD ?? '3',   10);\nconst CASCADE_WINDOW_SEC  = parseInt($env.CASCADE_WINDOW    ?? '60',  10);\n\nconst EFFECTIVE_TIMEOUT   = Number.isFinite(TIMEOUT_WINDOW_SEC)  && TIMEOUT_WINDOW_SEC  > 0 ? TIMEOUT_WINDOW_SEC  : 300;\nconst EFFECTIVE_THRESHOLD = Number.isFinite(CASCADE_THRESHOLD)   && CASCADE_THRESHOLD   > 0 ? CASCADE_THRESHOLD   : 3;\nconst EFFECTIVE_CASCADE   = Number.isFinite(CASCADE_WINDOW_SEC)  && CASCADE_WINDOW_SEC  > 0 ? CASCADE_WINDOW_SEC  : 60;\n\n// 1. TRANSITION TABLE 6\u00d76\nconst TRANSITIONS = {\n  INFO:          { sigma_info:'INFO',     sigma_warn:'WARNING',  sigma_error:'ERROR',    sigma_crit:'CRITICAL', sigma_ack:'INFO',     sigma_malformed:'INFO',          sigma_timeout:'INFO'  },\n  WARNING:       { sigma_info:'INFO',     sigma_warn:'WARNING',  sigma_error:'ERROR',    sigma_crit:'CRITICAL', sigma_ack:'WARNING',  sigma_malformed:'WARNING',       sigma_timeout:'INFO'  },\n  ERROR:         { sigma_info:'WARNING',  sigma_warn:'WARNING',  sigma_error:'ERROR',    sigma_crit:'CRITICAL', sigma_ack:'ERROR',    sigma_malformed:'ERROR',         sigma_timeout:'INFO'  },\n  ERROR_CASCADE: { sigma_info:'WARNING',  sigma_warn:'ERROR',    sigma_error:'ERROR',    sigma_crit:'CRITICAL', sigma_ack:'ERROR',    sigma_malformed:'ERROR_CASCADE', sigma_timeout:'INFO'  },\n  CRITICAL:      { sigma_info:'CRITICAL', sigma_warn:'CRITICAL', sigma_error:'CRITICAL', sigma_crit:'CRITICAL', sigma_ack:'RECOVERY', sigma_malformed:'CRITICAL',      sigma_timeout:'INFO'  },\n  RECOVERY:      { sigma_info:'INFO',     sigma_warn:'WARNING',  sigma_error:'ERROR',    sigma_crit:'CRITICAL', sigma_ack:'RECOVERY', sigma_malformed:'RECOVERY',      sigma_timeout:'INFO'  },\n};\n\n// 2. REGEX PATTERNS\nconst REGEX_PATTERNS = [\n  { symbol: 'sigma_crit',  pattern: /\\b(CRITICAL|FATAL|EMERGENCY|PANIC|EMERG|KERN_CRIT|SEV1|P1)\\b/i },\n  { symbol: 'sigma_error', pattern: /\\b(ERROR|Exception|Traceback|FAILED|FAILURE|5[0-9]{2}|NullPointer|OutOfMemory|Segfault|SIGSEGV|SIGABRT)\\b/i },\n  { symbol: 'sigma_warn',  pattern: /\\b(WARN|WARNING|SLOW|RETRY|DEPRECATED|TIMEOUT(?!_WINDOW)|HIGH_LATENCY|BACKPRESSURE|THROTTL)\\b/i },\n  { symbol: 'sigma_info',  pattern: /\\b(INFO|DEBUG|TRACE|OK|SUCCESS|STARTED|STOPPED|HEALTHY|2[0-9]{2}|CONNECTED|INITIALIZED)\\b/i },\n];\nconst ACK_PATTERN = /\\b(__AFD_ACK__)\\b/;\n\nfunction evaluateSymbol(message) {\n  if (message === null || message === undefined) return 'sigma_malformed';\n  if (typeof message !== 'string' || message.trim() === '') return 'sigma_malformed';\n  if (ACK_PATTERN.test(message)) return 'sigma_ack';\n  for (const { symbol, pattern } of REGEX_PATTERNS) {\n    if (pattern.test(message)) return symbol;\n  }\n  return 'sigma_malformed';\n}\n\nfunction applyTransition(state, symbol) {\n  const row = TRANSITIONS[state];\n  if (!row) return TRANSITIONS['INFO'][symbol] ?? 'INFO';\n  return row[symbol] ?? state;\n}\n\n// 3. READ LOG\nconst logObject = $('Insert rows in a table').item.json;\nconst sourceId  = logObject.source_id ?? 'unknown';\nconst message   = logObject.message   ?? '';\nconst logId     = logObject.id        ?? null;\nconst now       = new Date();\n\n// 4. READ AFD STATE\nconst stateRow = $json;\n\nlet rawState = stateRow?.current_state ?? 'INFO';\nif (rawState === 'NOMINAL')  rawState = 'INFO';\nif (rawState === 'DEGRADED') rawState = 'WARNING';\nlet currentState = TRANSITIONS[rawState] ? rawState : 'INFO';\n\nconst isNewService      = stateRow?.is_new_service !== false;\nconst cascadeCount      = parseInt(stateRow?.cascade_count ?? '0', 10) || 0;\nconst elapsedSec        = Number(stateRow?.elapsed_since_last_sec ?? 999999);\nconst cascadeElapsedSec = Number(stateRow?.cascade_elapsed_sec   ?? 999999);\n\n// 5. TEMPORAL RESET\nlet stateAfterTimeout = currentState;\nlet timeoutApplied    = false;\nlet newCascadeCount   = cascadeCount;\n\nif (!isNewService && elapsedSec > EFFECTIVE_TIMEOUT) {\n  stateAfterTimeout = applyTransition(currentState, 'sigma_timeout');\n  timeoutApplied    = true;\n  newCascadeCount   = 0;\n}\n\n// 6. SYMBOL + TRANSITION + CASCADE WINDOW\nconst symbol      = evaluateSymbol(message);\nconst rawNewState = applyTransition(stateAfterTimeout, symbol);\n\nlet newState      = rawNewState;\nlet cascade_reset = 'keep';\n\nif (rawNewState === 'ERROR') {\n  if (cascadeElapsedSec > EFFECTIVE_CASCADE) {\n    newCascadeCount = 1;\n    cascade_reset   = 'reset_now';\n    newState        = 'ERROR';\n  } else {\n    newCascadeCount = cascadeCount + 1;\n    cascade_reset   = 'keep';\n    if (newCascadeCount >= EFFECTIVE_THRESHOLD) {\n      newState = 'ERROR_CASCADE';\n    }\n  }\n} else if (rawNewState === 'ERROR_CASCADE') {\n  newCascadeCount = cascadeCount + 1;\n  cascade_reset   = 'keep';\n} else {\n  if (currentState === 'ERROR' || currentState === 'ERROR_CASCADE') {\n    newCascadeCount = 0;\n    cascade_reset   = 'reset_null';\n  }\n}\n\nconst previousState = currentState;\n\n// 7. OUTPUT\nreturn {\n  ...logObject,\n  afd_symbol:        symbol,\n  afd_state:         newState,\n  previous_state:    previousState,\n  source_id:         sourceId,\n  id:                logId,\n  cascade_count:     newCascadeCount,\n  cascade_reset:     cascade_reset,\n  is_new_service:    isNewService,\n  timeout_applied:   timeoutApplied,\n  elapsed_sec:       Math.round(elapsedSec),\n  cascade_elapsed:   Math.round(cascadeElapsedSec),\n  cascade_threshold: EFFECTIVE_THRESHOLD,\n  cascade_window:    EFFECTIVE_CASCADE,\n  engine_ts:         now.toISOString(),\n  engine_version:    'v2',\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        16,
        664
      ],
      "id": "c6e07d8d-e194-46b1-9f04-7375d0d64232",
      "name": "AFD Engine"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO afd_states (\n  source_id, current_state, previous_state, last_symbol,\n  last_event_time, cascade_count, cascade_start_time, updated_at\n)\nVALUES (\n  '{{ $json.source_id }}',\n  '{{ $json.afd_state }}',\n  '{{ $json.previous_state }}',\n  '{{ $json.afd_symbol }}',\n  NOW(),\n  {{ $json.cascade_count }},\n  CASE '{{ $json.cascade_reset }}'\n    WHEN 'reset_now' THEN NOW()\n    ELSE NULL\n  END,\n  NOW()\n)\nON CONFLICT (source_id) DO UPDATE SET\n  current_state      = EXCLUDED.current_state,\n  previous_state     = EXCLUDED.previous_state,\n  last_symbol        = EXCLUDED.last_symbol,\n  last_event_time    = NOW(),\n  cascade_count      = EXCLUDED.cascade_count,\n  cascade_start_time = CASE '{{ $json.cascade_reset }}'\n    WHEN 'reset_now'  THEN NOW()\n    WHEN 'reset_null' THEN NULL\n    ELSE afd_states.cascade_start_time\n  END,\n  updated_at         = NOW()",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        240,
        568
      ],
      "id": "d257c23d-ad18-4dd3-9893-99de30ee6932",
      "name": "AFD State Write",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "UPDATE logs SET afd_state = '{{ $json.afd_state }}', afd_symbol = '{{ $json.afd_symbol }}', previous_state = '{{ $json.previous_state }}' WHERE id = {{ $json.id }}",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        240,
        760
      ],
      "id": "45d058cf-dec0-48a6-90c8-ea3cee6a1811",
      "name": "Log State Write",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "id": "ab6ff2e0-ca5f-4b91-8970-f46690fb1d74",
                    "leftValue": "={{ ['INFO', 'NOMINAL'].includes($json.afd_state) }}",
                    "rightValue": "true",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "INFO Handler"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "id": "710fdff7-1a07-4f1a-aeea-f9404a85c721",
                    "leftValue": "={{ ['WARNING', 'DEGRADED'].includes($json.afd_state) }}",
                    "rightValue": "",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "WARNING Handler"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "id": "22be804b-0965-4e3a-adca-ab0d690c5965",
                    "leftValue": "={{$json.afd_state}}",
                    "rightValue": "ERROR",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "ERROR Handler"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "id": "9d97fd1e-49d1-401b-98ac-618d1b463c50",
                    "leftValue": "={{ $json.afd_state }}",
                    "rightValue": "RECOVERY",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "RECOVERY Handler"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "id": "ff408d4f-2f53-4e9a-bbfc-7d9bfa22e67d",
                    "leftValue": "={{ ['CRITICAL', 'ERROR_CASCADE'].includes($json.afd_state) }}",
                    "rightValue": "",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "CRITICAL Handler"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.4,
      "position": [
        240,
        280
      ],
      "id": "fc96383f-9471-4c62-a3f2-a7b21633786c",
      "name": "AFD State Switch Router"
    },
    {
      "parameters": {
        "fromEmail": "={{$env.SMTP_FROM}}",
        "toEmail": "={{$env.SMTP_TO}}",
        "subject": "=WARNING - {{$json.source_id}}",
        "emailFormat": "text",
        "text": "=Warning detected:\n{{$json.message}}\n\nState: WARNING\nTime: {{$now}}",
        "options": {}
      },
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        464,
        -32
      ],
      "id": "ac57f744-7aa5-440e-b173-48b76848e244",
      "name": "Send an Email",
      "credentials": {
        "smtp": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "fromEmail": "={{$env.SMTP_FROM}}",
        "toEmail": "={{$env.SMTP_TO}}",
        "subject": "=ERROR - {{$json.source_id}}",
        "emailFormat": "text",
        "text": "=Error detected:\n{{$json.message}}\n\nPlease investigate immediately.",
        "options": {}
      },
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        464,
        352
      ],
      "id": "8fe4de58-6c24-45f8-b43f-28602c20bd45",
      "name": "Send an Email1",
      "credentials": {
        "smtp": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "mode": "raw",
        "jsonOutput": "{\n  \"status\": \"WARNING\",\n  \"message\": \"email_sent\"\n}",
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        464,
        160
      ],
      "id": "f54d803f-8852-4a78-a310-7f40eeda7ada",
      "name": "WARNING Message"
    },
    {
      "parameters": {
        "mode": "raw",
        "jsonOutput": "{\n  \"status\": \"INFO\",\n  \"message\": \"processed\"\n}",
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        464,
        -224
      ],
      "id": "6090ebf2-f907-4655-9b93-4936354400eb",
      "name": "INFO Message"
    },
    {
      "parameters": {
        "mode": "raw",
        "jsonOutput": "{\n  \"status\": \"ERROR\",\n  \"message\": \"email_sent\"\n}",
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        464,
        544
      ],
      "id": "ce19b482-2c50-4fa1-b343-36d6461f9db2",
      "name": "ERROR Message"
    },
    {
      "parameters": {
        "mode": "raw",
        "jsonOutput": "{\n  \"status\": \"CRITICAL\",\n  \"message\": \"sent_to_llm\"\n}",
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        464,
        928
      ],
      "id": "09e3a44d-451a-48b5-abc3-1d2779b0ccae",
      "name": "CRITICAL Message"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={\n  \"status\": \"{{$json.status}}\",\n  \"message\": \"{{$json.message}}\"\n}",
        "options": {
          "responseCode": 200
        }
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        688,
        352
      ],
      "id": "ac744d7f-97e3-429e-b949-c6a2547e9867",
      "name": "Final Response"
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "logs",
        "responseMode": "responseNode",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        -1328,
        760
      ],
      "id": "e36483f8-796d-4c05-81db-824d57e890fb",
      "name": "Webhook1"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT message \nFROM logs WHERE source_id = '{{ $json.source_id }}'\nORDER BY log_timestamp DESC\nLIMIT 5;\n",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        912,
        1216
      ],
      "id": "f270d84f-9924-46b4-9220-c1192de7578d",
      "name": "Get Recent Logs",
      "alwaysOutputData": true,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const originalLog = $('AFD Engine').first().json;\nconst rows = $input.all();\nconst recent_logs = rows.map(r => r.json.message);\n\nreturn [{\n  json: {\n    ...originalLog,\n    recent_logs\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1136,
        1216
      ],
      "id": "f90d7c90-ffbf-4f90-bc74-d48a7cefb8ec",
      "name": "Format Recent Logs"
    },
    {
      "parameters": {
        "jsCode": "const log = $input.first().json;\n\nconst actions = Array.isArray(log.actions_immediates)\n  ? log.actions_immediates\n  : [];\n\nconst timestamp = new Date().toISOString();\n\n/**\n * Format all actions\n */\nconst actionsText = actions.length\n  ? actions.map((a, i) => `${i + 1}. ${a}`).join('\\n')\n  : \"N/A\";\n\n/**\n * WhatsApp Message \n */\nconst whatsappMessage = `\nCRITICAL \u2014 ${log.source_id}\n\n${log.message?.slice(0, 200)}\n\nCause probable:\n${log.cause_probable}\n\nActions:\n${actionsText}\n\n${timestamp}\n\nReply:\nACK \u2014 Prise en charge\nFALSE-POSITIVE \u2014 Faux positif\nCORRIGER-WARNING \u2014 Reclassifier warning\nCORRIGER-ERROR \u2014 Reclassifier error\nRESOLVED \u2014 Incident r\u00e9solu\nSTATUS \u2014 \u00c9tat du syst\u00e8me\nESCALATE \u2014 Escalader\nHELP \u2014 Aide\n`;\n\n/**\n * HTML Email (no emojis)\n */\nconst htmlMessage = `\n<h2>CRITICAL Alert</h2>\n\n<p><b>Service:</b> ${log.source_id}</p>\n\n<p><b>Message:</b><br>${log.message}</p>\n\n<p><b>Cause probable:</b><br>${log.cause_probable}</p>\n\n<p><b>Actions:</b></p>\n<ul>\n${actions.map(a => `<li>${a}</li>`).join('')}\n</ul>\n\n<p><b>Timestamp:</b> ${timestamp}</p>\n`;\n\nreturn [\n  {\n    json: {\n      ...log,\n      whatsappMessage,\n      htmlMessage,\n      alert_timestamp: timestamp\n    }\n  }\n];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2704,
        1024
      ],
      "id": "a02e5147-0ba2-49ea-9598-984eaa4cc01d",
      "name": "Format Alert Message"
    },
    {
      "parameters": {
        "fromEmail": "={{$env.SMTP_FROM}}",
        "toEmail": "={{$env.SMTP_TO}}",
        "subject": "=[CRITICAL] {{$json.source_id}} \u2014 {{$json.alert_timestamp}}",
        "html": "={{$json.htmlMessage}}",
        "options": {}
      },
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        2928,
        1024
      ],
      "id": "c4ebafc7-be43-482b-bc83-cb044e031fb9",
      "name": "Alert Email",
      "credentials": {
        "smtp": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "UPDATE afd_states\nSET ack_time = NULL\nWHERE source_id = '{{$json.source_id}}';",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        2928,
        1216
      ],
      "id": "57b52d44-56be-4fa6-a86e-ba8217dc0fa0",
      "name": "Reset ACK Time",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://api.twilio.com/2010-04-01/Accounts/{{$env.TWILIO_ACCOUNT_SID}}/Messages.json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth",
        "sendBody": true,
        "contentType": "form-urlencoded",
        "bodyParameters": {
          "parameters": [
            {
              "name": "From",
              "value": "={{$env.TWILIO_WHATSAPP_FROM}}"
            },
            {
              "name": "To",
              "value": "={{$env.TWILIO_WHATSAPP_ADMIN}}"
            },
            {
              "name": "Body",
              "value": "={{$json.whatsappMessage}}"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        2928,
        832
      ],
      "id": "5349fe24-c7f2-472e-87cd-684b862788de",
      "name": "Twilio WhatsApp Alert",
      "credentials": {
        "httpBasicAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT COUNT(*)::int AS critical_count\nFROM afd_states\nWHERE current_state = 'CRITICAL';",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        464,
        1120
      ],
      "id": "172155c9-06aa-46be-8ffe-84b578af774e",
      "name": "Count Critical Services",
      "alwaysOutputData": true,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "d1b82296-d5eb-4508-acdf-863762ba3966",
              "leftValue": "={{$json.critical_count >= 2}}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        688,
        1120
      ],
      "id": "36961dc3-b17e-4d8d-97aa-7aab47cb2615",
      "name": "System Wide Critical?",
      "alwaysOutputData": false
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT\n    source_id,\n    current_state,\n    last_event_time\nFROM afd_states\nWHERE current_state = 'CRITICAL'\nORDER BY last_event_time DESC;",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        1808,
        784
      ],
      "id": "e3fcfc2c-8690-4035-b804-be583779d880",
      "name": "Get All Critical Services",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const services = $input.all().map(item => ({\n  source_id: item.json.source_id,\n  state: item.json.current_state,\n  last_event_time: item.json.last_event_time\n}));\n\nconst prompt = `\nYou are an expert SRE incident correlation engine.\n\nMultiple services are simultaneously in CRITICAL state.\n\nAnalyze possible correlations and shared root causes.\n\nServices:\n${JSON.stringify(services, null, 2)}\n\nRespond ONLY in JSON:\n{\n  \"global_incident\": true,\n  \"possible_root_cause\": \"...\",\n  \"recommended_action\": \"...\",\n  \"severity\": \"HIGH\"\n}\n`;\n\nreturn [{\n  json: {\n    services,\n    llm_payload: {\n      model: $env.GROQ_MODEL ?? 'llama-3.1-8b-instant',\n      messages: [\n        {\n          role: 'user',\n          content: prompt\n        }\n      ],\n      temperature: 0.2,\n      max_tokens: 250,\n      response_format: {\n        type: \"json_object\"\n      }\n    }\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2032,
        784
      ],
      "id": "b0ac7166-17c4-41b9-b809-97e6dceb61f6",
      "name": "Build Correlation Prompt"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{$env.GROQ_API_URL}}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{$json.llm_payload}}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        2256,
        784
      ],
      "id": "fc8e7cbf-f2d8-4e74-94d4-97a6c3c85ca5",
      "name": "Groq Correlation Call",
      "credentials": {
        "httpBearerAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const httpResult = $json;\n\nif (httpResult.error || !httpResult.choices) {\n  return [{\n    json: {\n      global_incident: true,\n      possible_root_cause: 'Unable to determine root cause',\n      recommended_action: 'Manual investigation required',\n      severity: 'HIGH',\n      llm_source: 'fallback'\n    }\n  }];\n}\n\ntry {\n  const rawText = httpResult.choices[0].message.content.trim();\n  const parsed = JSON.parse(rawText);\n\n  return [{\n    json: {\n      global_incident: parsed.global_incident ?? true,\n      possible_root_cause: parsed.possible_root_cause,\n      recommended_action: parsed.recommended_action,\n      severity: parsed.severity ?? 'HIGH',\n      llm_source: 'groq'\n    }\n  }];\n} catch (e) {\n  return [{\n    json: {\n      global_incident: true,\n      possible_root_cause: 'Correlation parsing failed',\n      recommended_action: 'Investigate all affected services',\n      severity: 'HIGH',\n      llm_source: 'fallback'\n    }\n  }];\n}"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2480,
        784
      ],
      "id": "20b730e3-0ae4-46ff-b0d6-a0fe8c8863fd",
      "name": "Parse Correlation Response"
    },
    {
      "parameters": {
        "mode": "raw",
        "jsonOutput": "={\n  \"whatsappMessage\": \"SYSTEM-WIDE CRITICAL\\n\\nMultiple services are simultaneously failing.\\n\\nPossible root cause:\\n{{$json.possible_root_cause}}\\n\\nRecommended action:\\n{{$json.recommended_action}}\"\n}",
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        2704,
        784
      ],
      "id": "91791b61-7f2e-4007-9183-de26f15ed293",
      "name": "System Wide Alert Message"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT\n    EXISTS (\n        SELECT 1\n        FROM llm_cache\n        WHERE message_hash = '{{ $json.message_hash }}'\n    ) AS cache_found,\n    (\n        SELECT response_text\n        FROM llm_cache\n        WHERE message_hash = '{{ $json.message_hash }}'\n        LIMIT 1\n    ) AS response_text,\n    (\n        SELECT llm_source\n        FROM llm_cache\n        WHERE message_hash = '{{ $json.message_hash }}'\n        LIMIT 1\n    ) AS llm_source,\n    (\n        SELECT hit_count\n        FROM llm_cache\n        WHERE message_hash = '{{ $json.message_hash }}'\n        LIMIT 1\n    ) AS hit_count;",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        1584,
        1216
      ],
      "id": "df98f7fb-7203-49e0-ab1a-7c2a5f40666e",
      "name": "Check LLM Cache",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "83cd8edb-9bc2-4e12-922e-60adb18e763c",
              "leftValue": "={{ $json.response_text }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        1808,
        1216
      ],
      "id": "2828523c-7d97-43c3-b96e-5f42ec6e7c25",
      "name": "Cache Found?"
    },
    {
      "parameters": {
        "jsCode": "const originalLog = $('Build LLM Cache Key').first().json;\nconst cacheRow = $input.first().json;\n\nlet response;\n\ntry {\n  response = JSON.parse(cacheRow.response_text);\n} catch (e) {\n  response = {\n    cause_probable: cacheRow.response_text,\n    actions_immediates: ['V\u00e9rifier le service concern\u00e9 et consulter les logs r\u00e9cents.'],\n    escalade_si: 'Si le probl\u00e8me persiste',\n    severite_estimee: 'CRITICAL'\n  };\n}\n\nreturn [{\n  json: {\n    ...originalLog,\n    ...response,\n    llm_source: cacheRow.llm_source || 'cache',\n    llm_cache_hit: true\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2480,
        976
      ],
      "id": "2cfc1ad3-8b55-47d5-b37b-9461bb76363b",
      "name": "Use Cached LLM Response"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "UPDATE llm_cache\nSET\n    hit_count = hit_count + 1,\n    last_hit_at = NOW()\nWHERE message_hash = '{{ $json.message_hash }}';",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        2704,
        1216
      ],
      "id": "fbb352c6-a66c-41ca-918b-d93e23a1730e",
      "name": "Update LLM Cache Hit",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO llm_cache (\n    message_hash,\n    category,\n    afd_state,\n    response_text,\n    llm_source,\n    created_at,\n    last_hit_at,\n    hit_count\n)\nVALUES (\n    '{{ $json.message_hash }}',\n    '{{ $json.category }}',\n    '{{ $json.afd_state }}',\n    '{{ JSON.stringify({\n        cause_probable: $json.cause_probable,\n        actions_immediates: $json.actions_immediates,\n        escalade_si: $json.escalade_si,\n        severite_estimee: $json.severite_estimee\n    }).replace(/'/g, \"''\") }}',\n    '{{ $json.llm_source }}',\n    NOW(),\n    NOW(),\n    1\n)\nON CONFLICT (message_hash)\nDO UPDATE SET\n    last_hit_at = NOW(),\n    hit_count = llm_cache.hit_count + 1;",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        2704,
        1408
      ],
      "id": "f0ff6fef-55ba-4b37-b968-dd924b0ef916",
      "name": "Store LLM Cache",
      "alwaysOutputData": true,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const log = $('Build LLM Prompt').first().json;\n\nconst staticFallbacks = {\n  SECURITY:     'V\u00e9rifier les logs d\\'acc\u00e8s, bloquer l\\'IP source, notifier l\\'\u00e9quipe s\u00e9curit\u00e9.',\n  PERFORMANCE:  'V\u00e9rifier CPU/RAM/disque, identifier le processus fautif, red\u00e9marrer si n\u00e9cessaire.',\n  AVAILABILITY: 'V\u00e9rifier la connectivit\u00e9 r\u00e9seau, l\\'\u00e9tat du service et de ses d\u00e9pendances.',\n  DATA:         'V\u00e9rifier l\\'int\u00e9grit\u00e9 des donn\u00e9es, stopper les \u00e9critures, alerter l\\'\u00e9quipe DBA.',\n  NETWORK:      'V\u00e9rifier la connectivit\u00e9, les r\u00e8gles firewall, et les DNS.',\n  SYSTEM:       'V\u00e9rifier les ressources syst\u00e8me, les logs kernel, et l\\'espace disque.'\n};\n\nfunction staticResponse(category) {\n  return {\n    cause_probable:     'LLM indisponible \u2014 r\u00e9ponse statique',\n    actions_immediates: [staticFallbacks[category] || 'Analyser les logs et escalader.'],\n    escalade_si:        'Si le probl\u00e8me persiste plus de 10 minutes',\n    severite_estimee:   'CRITICAL',\n    llm_source:         'static'\n  };\n}\n\n// $json ici = r\u00e9ponse du HTTP Request node (ou objet erreur si \"Continue on Fail\")\nconst httpResult = $json;\n\n// Si le n\u0153ud HTTP a \u00e9chou\u00e9, $json contiendra un champ \"error\"\nif (httpResult.error || !httpResult.choices) {\n  return [{ json: { ...log, ...staticResponse(log.category) } }];\n}\n\ntry {\n  const rawText  = httpResult.choices[0].message.content.trim();\n  const parsed   = JSON.parse(rawText);\n\n  return [{\n    json: {\n      ...log,\n      cause_probable:     parsed.cause_probable,\n      actions_immediates: parsed.actions_immediates,\n      escalade_si:        parsed.escalade_si,\n      severite_estimee:   parsed.severite_estimee,\n      llm_source:         'groq'\n    }\n  }];\n} catch (e) {\n  return [{ json: { ...log, ...staticResponse(log.category) } }];\n}"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2480,
        1360
      ],
      "id": "711697cc-d1ef-4be5-9e7d-1d1b1d8c4009",
      "name": "Parse LLM Response1"
    },
    {
      "parameters": {
        "jsCode": "function simpleHash(input) {\n  let hash1 = 0x811c9dc5;\n  let hash2 = 0x01000193;\n\n  for (let i = 0; i < input.length; i++) {\n    const char = input.charCodeAt(i);\n\n    hash1 ^= char;\n    hash1 = Math.imul(hash1, 16777619);\n\n    hash2 ^= char;\n    hash2 = Math.imul(hash2, 2166136261);\n  }\n\n  const part1 = (hash1 >>> 0).toString(16).padStart(8, '0');\n  const part2 = (hash2 >>> 0).toString(16).padStart(8, '0');\n\n  return (part1 + part2 + part1 + part2 + part1 + part2 + part1 + part2).substring(0, 64);\n}\n\nreturn items.map(item => {\n  const log = item.json;\n\n  const message = String(log.message ?? log.raw_log ?? '');\n\n  const category = String(log.category ?? 'SYSTEM').toUpperCase();\n\n  const afdState = String(log.afd_state ?? 'CRITICAL').toUpperCase();\n\n  const normalizedMessage = message\n    .toLowerCase()\n    .replace(/\\d{4}-\\d{2}-\\d{2}t?\\s?\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?z?/gi, '<ts>')\n    .replace(/\\d{4}-\\d{2}-\\d{2}\\s+\\d{2}:\\d{2}:\\d{2}/g, '<ts>')\n    .replace(/\\b\\d{1,3}(\\.\\d{1,3}){3}\\b/g, '<ip>')\n    .replace(/:\\d{4,5}\\b/g, ':<port>')\n    .replace(/\\s+/g, ' ')\n    .trim()\n    .substring(0, 256);\n\n  const hashInput = `${normalizedMessage}|${category}|${afdState}`;\n  const messageHash = simpleHash(hashInput);\n\n  return {\n    json: {\n      ...log,\n      category: category,\n      afd_state: afdState,\n      normalized_message: normalizedMessage,\n      message_hash: messageHash,\n      llm_cache_hit: false\n    }\n  };\n});"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1360,
        1216
      ],
      "id": "7b6a66fd-6a43-48cf-8c9a-f21043e630a8",
      "name": "Build LLM Cache Key"
    },
    {
      "parameters": {
        "jsCode": "const log = $('Build LLM Cache Key').first().json;\n\nconst userPrompt = `Tu es un ing\u00e9nieur SRE expert en monitoring.\nUn log CRITICAL a \u00e9t\u00e9 d\u00e9tect\u00e9 dans le service \"${log.source_id}\".\nLog : ${log.message}\nCat\u00e9gorie : ${log.category || 'SYSTEM'}\nHistorique r\u00e9cent :\n${JSON.stringify(log.recent_logs || [])}\n\nR\u00e9ponds UNIQUEMENT en JSON valide, sans texte avant ou apr\u00e8s :\n{\n  \"cause_probable\": \"...\",\n  \"actions_immediates\": [\"action 1\", \"action 2\", \"action 3\"],\n  \"escalade_si\": \"...\",\n  \"severite_estimee\": \"HIGH\"\n}\nMaximum 150 mots.`;\n\nreturn [{\n  json: {\n    ...log,\n    llm_payload: {\n      model: $env.GROQ_MODEL ?? 'llama-3.1-8b-instant',\n      messages: [{ role: 'user', content: userPrompt }],\n      max_tokens: 300,\n      temperature: 0.3,\n      response_format: { type: \"json_object\" }\n    }\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2032,
        1360
      ],
      "id": "841455c3-8cfb-43ab-83f5-97e8c8a8e5d9",
      "name": "Build LLM Prompt"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{$env.GROQ_API_URL}}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{$json.llm_payload}}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        2256,
        1360
      ],
      "id": "172ab3ed-1ddb-45a7-9441-f93b21ea5140",
      "name": "Groq API Call",
      "credentials": {
        "httpBearerAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "mode": "raw",
        "jsonOutput": "{\n  \"status\": \"RECOVERY\",\n  \"message\": \"monitoring_post_incident\"\n}",
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        464,
        736
      ],
      "id": "729b943a-90b4-414c-9fb5-5a9f9488a43a",
      "name": "RECOVERY Message"
    }
  ],
  "connections": {
    "Format Detector": {
      "main": [
        [
          {
            "node": "Switch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Switch": {
      "main": [
        [
          {
            "node": "JSON Processor",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Syslog Processor",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Apache Processor",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Plaintext Processor",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Unknown Processor",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JSON Processor": {
      "main": [
        [
          {
            "node": "Insert rows in a table",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Syslog Processor": {
      "main": [
        [
          {
            "node": "Insert rows in a table",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Apache Processor": {
      "main": [
        [
          {
            "node": "Insert rows in a table",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Plaintext Processor": {
      "main": [
        [
          {
            "node": "Insert rows in a table",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Unknown Processor": {
      "main": [
        [
          {
            "node": "Insert rows in a table",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Insert rows in a table": {
      "main": [
        [
          {
            "node": "AFD State Read",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AFD State Read": {
      "main": [
        [
          {
            "node": "AFD Engine",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AFD Engine": {
      "main": [
        [
          {
            "node": "AFD State Write",
            "type": "main",
            "index": 0
          },
          {
            "node": "Log State Write",
            "type": "main",
            "index": 0
          },
          {
            "node": "AFD State Switch Router",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AFD State Switch Router": {
      "main": [
        [
          {
            "node": "INFO Message",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send an Email",
            "type": "main",
            "index": 0
          },
          {
            "node": "WARNING Message",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send an Email1",
            "type": "main",
            "index": 0
          },
          {
            "node": "ERROR Message",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "RECOVERY Message",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "CRITICAL Message",
            "type": "main",
            "index": 0
          },
          {
            "node": "Count Critical Services",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "INFO Message": {
      "main": [
        [
          {
            "node": "Final Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "WARNING Message": {
      "main": [
        [
          {
            "node": "Final Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "ERROR Message": {
      "main": [
        [
          {
            "node": "Final Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "CRITICAL Message": {
      "main": [
        [
          {
            "node": "Final Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook1": {
      "main": [
        [
          {
            "node": "Format Detector",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Recent Logs": {
      "main": [
        [
          {
            "node": "Format Recent Logs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Recent Logs": {
      "main": [
        [
          {
            "node": "Build LLM Cache Key",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Alert Message": {
      "main": [
        [
          {
            "node": "Alert Email",
            "type": "main",
            "index": 0
          },
          {
            "node": "Reset ACK Time",
            "type": "main",
            "index": 0
          },
          {
            "node": "Twilio WhatsApp Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Count Critical Services": {
      "main": [
        [
          {
            "node": "System Wide Critical?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "System Wide Critical?": {
      "main": [
        [
          {
            "node": "Get All Critical Services",
            "type": "main",
            "index": 0
          },
          {
            "node": "Get Recent Logs",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Get Recent Logs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get All Critical Services": {
      "main": [
        [
          {
            "node": "Build Correlation Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Correlation Prompt": {
      "main": [
        [
          {
            "node": "Groq Correlation Call",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Groq Correlation Call": {
      "main": [
        [
          {
            "node": "Parse Correlation Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Correlation Response": {
      "main": [
        [
          {
            "node": "System Wide Alert Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "System Wide Alert Message": {
      "main": [
        [
          {
            "node": "Twilio WhatsApp Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check LLM Cache": {
      "main": [
        [
          {
            "node": "Cache Found?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Cache Found?": {
      "main": [
        [
          {
            "node": "Use Cached LLM Response",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build LLM Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Use Cached LLM Response": {
      "main": [
        [
          {
            "node": "Update LLM Cache Hit",
            "type": "main",
            "index": 0
          },
          {
            "node": "Format Alert Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse LLM Response1": {
      "main": [
        [
          {
            "node": "Store LLM Cache",
            "type": "main",
            "index": 0
          },
          {
            "node": "Format Alert Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build LLM Cache Key": {
      "main": [
        [
          {
            "node": "Check LLM Cache",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build LLM Prompt": {
      "main": [
        [
          {
            "node": "Groq API Call",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Groq API Call": {
      "main": [
        [
          {
            "node": "Parse LLM Response1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "RECOVERY Message": {
      "main": [
        [
          {
            "node": "Final Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate"
  },
  "versionId": "e2baf941-d710-4b2b-8d40-8705ba130d95",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "id": "xFnXopS8rxxWf7od",
  "tags": []
}