{
  "name": "RAG Stack - Synchronisation corpus (Schedule + Email quarantaine)",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours"
            }
          ]
        }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        -656,
        64
      ],
      "name": "Schedule Trigger (toutes les heures)"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://rag-api:8080/admin/sync",
        "options": {
          "timeout": 1800000
        },
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        -448,
        64
      ],
      "name": "POST /admin/sync",
      "credentials": {
        "httpHeaderAuth": {
          "id": "<HEADER_AUTH_CREDENTIAL_ID>",
          "name": "RAG API - ADMIN_TOKEN"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 1
          },
          "conditions": [
            {
              "leftValue": "={{ $json.success }}",
              "rightValue": false,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "id": "7df49421-984c-4012-8ed0-27a03b6f9d8b"
            },
            {
              "leftValue": "={{ $json.quarantine_count }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "gt"
              },
              "id": "ae9780f3-0948-4596-baa4-d525b17429f4"
            },
            {
              "leftValue": "={{ $json.cloisonnement_alerte === true }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "id": "348ca6a2-f963-4bfe-ad5e-bcff8eab374a"
            }
          ],
          "combinator": "or"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        -224,
        64
      ],
      "name": "Erreur, quarantaine ou cloisonnement ?"
    },
    {
      "parameters": {
        "jsCode": "const r = $input.first().json;\nconst ts = new Date(r.timestamp).toLocaleString('fr-CH', { timeZone: 'Europe/Zurich' });\nconst jour = new Date().toISOString().split('T')[0];\nconst cl = r.cloisonnement;\n\nlet sujet, corps = '';\n\n// ── Section cloisonnement (prioritaire : une fuite est l'alerte la plus grave)\nlet sectionCloisonnement = '';\nif (cl && r.cloisonnement_alerte) {\n  sectionCloisonnement += `CONTRÔLE DE CLOISONNEMENT : ${cl.ok} cas conformes sur ${cl.cas}\\n\\n`;\n  if (cl.fuites.length) {\n    sectionCloisonnement += `FUITES (document interdit accessible) :\\n`;\n    for (const f of cl.fuites) sectionCloisonnement += `  • ${f}\\n`;\n    sectionCloisonnement += `\\n→ Action immédiate : arrêter le service tant que la cause n'est pas comprise\\n`;\n    sectionCloisonnement += `  (cd /root/rag-stack && docker compose stop rag-api).\\n\\n`;\n  }\n  if (cl.refus_a_tort.length) {\n    sectionCloisonnement += `Refus à tort (document autorisé inaccessible) :\\n`;\n    for (const f of cl.refus_a_tort) sectionCloisonnement += `  • ${f}\\n`;\n    sectionCloisonnement += `\\n`;\n  }\n  if (cl.absents.length) {\n    sectionCloisonnement += `Cas invalides (document de test absent de l'index) :\\n`;\n    for (const f of cl.absents) sectionCloisonnement += `  • ${f}\\n`;\n    sectionCloisonnement += `\\n`;\n  }\n  if (cl.erreurs.length) {\n    sectionCloisonnement += `Erreurs du contrôle :\\n`;\n    for (const f of cl.erreurs) sectionCloisonnement += `  • ${f}\\n`;\n    sectionCloisonnement += `\\n`;\n  }\n  sectionCloisonnement += `Détail sur la VM :\\n`;\n  sectionCloisonnement += `  docker exec rag-api python3 /rag-pipeline/test_cloisonnement.py /rag-pipeline/cas_cloisonnement.json\\n\\n`;\n}\n\nif (cl && cl.fuites && cl.fuites.length) {\n  sujet = `🚨 [RAG SYNC] FUITE DE CLOISONNEMENT : ${jour}`;\n} else if (!r.success) {\n  sujet = `⚠️ [RAG SYNC] Erreur de synchronisation : ${jour}`;\n} else if (r.cloisonnement_alerte) {\n  sujet = `⚠️ [RAG SYNC] Contrôle de cloisonnement à examiner : ${jour}`;\n} else {\n  sujet = `📋 [RAG SYNC] Fichiers en quarantaine : ${jour}`;\n}\n\ncorps += `RAPPORT DE SYNCHRONISATION RAG\\nHorodatage : ${ts}\\n\\n`;\ncorps += sectionCloisonnement;\n\nif (!r.success) {\n  corps += `ERREUR DE SYNCHRONISATION\\n`;\n  corps += `Erreurs détectées :\\n`;\n  for (const e of r.errors) corps += `  • ${e}\\n`;\n  corps += `\\nCodes de retour : indexer.py = ${r.indexer?.returncode ?? 'non exécuté'}, `;\n  corps += `acl_resolver.py = ${r.acl_resolver?.returncode ?? 'non exécuté'}`;\n  if (r.sharepoint && r.sharepoint.returncode !== undefined) {\n    corps += `, sp_indexer.py = ${r.sharepoint.returncode}`;\n  }\n  corps += `\\n\\nDétail sur la VM :\\n`;\n  corps += `  docker logs rag-api 2>&1 | grep SYNC | tail -20\\n`;\n  corps += `  ls -la /var/log/rag/rapport_*.json\\n\\n`;\n}\n\nif (r.quarantine_count > 0) {\n  corps += `QUARANTAINE : ${r.quarantine_count} fichier(s)\\n`;\n  for (const f of r.quarantine) corps += `  • ${f}\\n`;\n  corps += `\\nCes fichiers sont présents sur le partage, mais ne sont pas interrogeables dans le RAG.\\n`;\n  corps += `Deux causes possibles, avec des actions différentes :\\n\\n`;\n  corps += `1. Permissions illisibles (statut « acl_illisible ») : les fichiers sont indexés, mais le compte\\n`;\n  corps += `   d'indexation ne peut plus lire leurs permissions NTFS. Par sécurité, ils sont devenus\\n`;\n  corps += `   invisibles pour tous. Cause la plus fréquente : un droit de lecture perdu, par exemple après\\n`;\n  corps += `   une modification des groupes du compte ou un dossier créé avec l'héritage coupé.\\n`;\n  corps += `   → Sur le serveur de fichiers : .\\\\Set-AccesIndexationRAG.ps1 -Racine <racine> -Verifier\\n`;\n  corps += `   → Après correction : remonter le partage sur la VM, puis relancer la synchronisation.\\n\\n`;\n  corps += `2. Fichier non indexé (statut « non_indexé ») : aucun contenu n'a pu être extrait\\n`;\n  corps += `   (fichier vide, PDF scanné sans texte, format non pris en charge, document chiffré).\\n`;\n  corps += `   → Vérifier le contenu du fichier.\\n\\n`;\n  corps += `Statut exact de chaque fichier sur la VM :\\n`;\n  corps += `  python3 -c \"import json; [print(d['statut'], '|', d.get('fichier', d.get('source_name'))) for d in json.load(open('/var/log/rag/rapport_acl.json')).get('documents', []) if d.get('statut') in ('non_indexe', 'non_indexé', 'acl_illisible')]\"\\n`;\n}\n\nreturn [{ json: { sujet, corps } }];\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        0,
        0
      ],
      "name": "Formater email"
    },
    {
      "parameters": {
        "fromEmail": "<EMAIL_EXPEDITEUR>",
        "toEmail": "<EMAIL_DESTINATAIRE>",
        "subject": "={{ $json.sujet }}",
        "emailFormat": "text",
        "text": "={{ $json.corps }}",
        "options": {
          "appendAttribution": false
        }
      },
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        224,
        0
      ],
      "name": "Email notification",
      "credentials": {
        "smtp": {
          "id": "<SMTP_CREDENTIAL_ID>",
          "name": "SMTP account"
        }
      }
    },
    {
      "parameters": {
        "url": "=http://<NOM-COMPLET-SUPERVISION>:3001/api/push/<CLE-PUSH>?status={{ $json.success ? 'up' : 'down' }}&msg={{ $json.success ? 'OK' : 'echec' }}&ping=",
        "options": {
          "timeout": 10000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        -224,
        240
      ],
      "name": "Signal supervision (Push)",
      "onError": "continueRegularOutput",
      "notes": "Appel Push de la supervision externe (§16.10) : up si la synchronisation a réussi, down sinon. Sans appel pendant 65 minutes, la supervision alerte. Ne bloque jamais le courriel d'erreur (On Error : Continue). Adresse : nom DNS COMPLET de la machine de supervision (ex. supervision.votre-domaine.ch) : le conteneur n8n ne résout pas les noms courts.",
      "disabled": true
    }
  ],
  "pinData": {},
  "connections": {
    "Schedule Trigger (toutes les heures)": {
      "main": [
        [
          {
            "node": "POST /admin/sync",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "POST /admin/sync": {
      "main": [
        [
          {
            "node": "Erreur, quarantaine ou cloisonnement ?",
            "type": "main",
            "index": 0
          },
          {
            "node": "Signal supervision (Push)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Formater email": {
      "main": [
        [
          {
            "node": "Email notification",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Erreur, quarantaine ou cloisonnement ?": {
      "main": [
        [
          {
            "node": "Formater email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate"
  },
  "nodeGroups": [],
  "tags": []
}