From 290f8fcd360023e626d77db013d4be7da7fcaf56 Mon Sep 17 00:00:00 2001 From: bolbol Date: Fri, 21 Aug 2026 20:18:21 +0100 Subject: [PATCH] feat(monitoring): ajout du referentiel Baserow infra_versions et du workflow n8n veille-versions-stack --- common/veille-versions-stack.md | 99 +++ .../workflows/veille-versions-stack.n8n.json | 831 ++++++++++++++++++ 2 files changed, 930 insertions(+) create mode 100644 common/veille-versions-stack.md create mode 100644 common/workflows/veille-versions-stack.n8n.json diff --git a/common/veille-versions-stack.md b/common/veille-versions-stack.md new file mode 100644 index 0000000..04b0cce --- /dev/null +++ b/common/veille-versions-stack.md @@ -0,0 +1,99 @@ +# Veille Versions Stack NAS & Référentiel Baserow + +Ce document décrit le fonctionnement du dispositif de suivi et de veille automatisée des versions des conteneurs du NAS Synology. + +> **Doctrine stricte** : Ce workflow ne fait **que détecter et notifier**. Aucune mise à jour n'est exécutée automatiquement. Toute opération de mise à jour fait l'objet d'un brief de cadrage, d'une exécution contrôlée avec sauvegardes et d'une validation par commandes réelles. + +--- + +## 1. Architecture du Dispositif + +```mermaid +graph TD + Trigger["Schedule Trigger (Lundi 08h00 Africa/Tunis)"] --> ReadBR["Lire Table Baserow (Table 1097 / Infra Stack)"] + ReadBR --> SSHCheck["SSH Restreint NAS (n8n-versions-ro)"] + SSHCheck --> MapItems["Préparer Mapping & Éléments"] + MapItems --> DHub["API Docker Hub (Tags stables)"] + DHub --> Analyze["Analyser Écarts & Versions"] + Analyze --> UpdateBR["Mettre à jour Baserow (PATCH Table 1097)"] + UpdateBR --> Filter["Filtrer Alertes (Si nouvel écart détecté)"] + Filter -->|Nouvel écart| Telegram["Notification Telegram (ND_Telegram)"] + Filter -->|À jour ou En pause| Silent["Silence (0 spam)"] +``` + +--- + +## 2. Composants Techniques + +### A. Référentiel Baserow (`infra_versions`) +- **Workspace** : `148 (Hors-RLA)` +- **Database** : `Infra Stack` (ID `317`) +- **Table** : `infra_versions` (ID `1097`) +- **URL** : `https://baserow.bolbol.tn/database/317/table/1097` +- **Champs suivis** : + - `conteneur` (Texte, clé primaire) : Nom du conteneur + - `image_repo` (Texte) : Repository Docker Hub (ex: `n8nio/n8n`, `maximhq/bifrost`) + - `methode_verif` (Single select) : `image_tag` ou `exec_version` + - `version_installee` (Texte) : Version réelle active sur le NAS + - `derniere_version_disponible` (Texte) : Dernière version stable publiée + - `ecart` (Texte) : Statut de l'écart (`0`, `Installée: ... ➔ Dispo: ...`) + - `criticite` (Single select) : `Critique`, `Important`, `Mineur` + - `date_derniere_verification` (Date) : Date du dernier passage de veille + - `date_derniere_maj_effective` (Date) : Date de la dernière MAJ appliquée + - `statut` (Single select) : `À jour`, `Retard mineur`, `Retard à traiter`, `En pause` + - `lien_runbook` (URL) : Lien vers le guide d'exploitation / runbook + - `notes` (Texte long) : Particularités techniques (ex: image custom, base de données, pins) + +--- + +### B. Accès SSH Restreint NAS (`n8n-versions-ro`) +Pour respecter le principe de moindre privilège et interdire tout shell interactif ou injection de commande : +- **Compte DSM** : `n8n-versions-ro` (UID `1041`, membre du groupe `docker` `65538`). +- **Clé SSH** : Clé Ed25519 dédiée (`/volume1/docker/scripts/id_ed25519_n8n_versions`). +- **Restriction `authorized_keys`** : + ```text + command="/volume1/docker/scripts/versions-check.sh",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIn3e1VvE3Z/q1W5o6Q+30w7U57u3aK8JqC8Z57Z8z/c n8n-versions-ro + ``` +- **Script exécutable** : `/volume1/docker/scripts/versions-check.sh` + - Filtre `$SSH_ORIGINAL_COMMAND` contre une whitelist stricte des 9 conteneurs. + - Toute commande arbitraire (`rm -rf /`, `sh`, etc.) est rejetée avec un code d'erreur `1`. + +--- + +### C. Workflow n8n (`veille-versions-stack`) +- **ID Workflow** : `zUWN5K4Q1A2JKFIS` +- **Déclenchement** : Hebdomadaire (Lundi à 08h00 Africa/Tunis) + Webhook de test manuel `/webhook/test-veille-versions`. +- **Canal Telegram** : Credential `ND_Telegram` (`OyOHh6jAzCDsiBux`), Chat ID `2084513684`. +- **Export JSON** : [`veille-versions-stack.n8n.json`](file:///tmp/nas-runbooks/common/workflows/veille-versions-stack.n8n.json) + +--- + +## 3. Table des 9 Conteneurs Suivis + +| Conteneur | Image Repo | Méthode | Criticité | Particularités | +|---|---|---|---|---| +| `n8n` | `n8nio/n8n` | `exec_version` | Important | Image custom (docx, exceljs) | +| `bifrost` | `maximhq/bifrost` | `image_tag` | Critique | Pin image, backup config.db/config.json | +| `gitea` | `gitea/gitea` | `image_tag` | Important | Pin rootless, backup natif `gitea dump` | +| `portainer` | `portainer/portainer-ce` | `image_tag` | Mineur | Pilote stacks, MAJ en priorité | +| `vaultwarden` | `vaultwarden/server` | `image_tag` | Mineur | Pin alpine, backup db.sqlite3 | +| `baserow` | `baserow/baserow` | `exec_version` | Important | Mode minimal, DNS dynamique Nginx | +| `crowdsec` | `crowdsecurity/crowdsec` | `exec_version` | Important | Sécurité IDS/IPS DSM | +| `tailscale-nyora-bridge` | `tailscale/tailscale` | `image_tag` | Mineur | Bridge HTTP/SOCKS5 | +| `trilium` | `triliumnext/trilium` | `image_tag` | Mineur | `En pause` (exclu des alertes actives) | + +--- + +## 4. Procédure de Test et Validation + +1. **Test d'exécution manuelle sans alerte (tout à jour)** : + ```bash + curl -s http://192.168.100.33:5678/webhook/test-veille-versions + # Réponse attendue : {"no_alert": true, "message": "Aucun nouvel ecart"} + ``` + +2. **Test d'interrogation SSH unitaire** : + ```bash + ssh -p 22222 -i /volume1/docker/scripts/id_ed25519_n8n_versions -o BatchMode=yes n8n-versions-ro@127.0.0.1 "n8n" + # Réponse attendue : version exacte (ex: 2.36.5) + ``` diff --git a/common/workflows/veille-versions-stack.n8n.json b/common/workflows/veille-versions-stack.n8n.json new file mode 100644 index 0000000..d9f4890 --- /dev/null +++ b/common/workflows/veille-versions-stack.n8n.json @@ -0,0 +1,831 @@ +{ + "id": "zUWN5K4Q1A2JKFIS", + "name": "veille-versions-stack", + "description": null, + "active": true, + "activeVersionId": "c2da509a-651c-4fa9-9aa8-00f4ab440123", + "createdAt": "2026-08-21T19:10:15.587Z", + "updatedAt": "2026-08-21T19:16:24.466Z", + "isArchived": false, + "versionId": "c2da509a-651c-4fa9-9aa8-00f4ab440123", + "versionCounter": 7, + "sourceWorkflowId": null, + "triggerCount": 2, + "nodes": [ + { + "parameters": { + "rule": { + "interval": [ + { + "field": "weeks", + "triggerAtHour": 8, + "triggerAtMinute": 0 + } + ] + } + }, + "id": "1", + "name": "Schedule Trigger (Lundi 08h00)", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.2, + "position": [ + 200, + 300 + ] + }, + { + "parameters": { + "url": "http://baserow:80/api/database/rows/table/1097/?user_field_names=true&size=100", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Host", + "value": "baserow.bolbol.tn" + }, + { + "name": "Authorization", + "value": "Token qS5KEYki5sA1VEdCn9hobXk0Bp4jd9fr" + } + ] + }, + "options": {} + }, + "id": "2", + "name": "Lire Table Baserow", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 420, + 300 + ] + }, + { + "parameters": { + "command": "all", + "authentication": "privateKey" + }, + "id": "3", + "name": "SSH Versions NAS (Restreint)", + "type": "n8n-nodes-base.ssh", + "typeVersion": 1, + "position": [ + 640, + 300 + ], + "credentials": { + "sshPrivateKey": { + "id": "UjDRfssNSG2A9Ku9", + "name": "SSH n8n-versions-ro" + } + } + }, + { + "parameters": { + "jsCode": "\nconst baserowRows = $('Lire Table Baserow').first().json.results;\nconst sshOutput = $input.first().json.stdout;\nlet installedVersions = {};\ntry {\n installedVersions = JSON.parse(sshOutput);\n} catch (e) {\n // fallback parsing\n installedVersions = {};\n}\n\nconst items = [];\nfor (const row of baserowRows) {\n const conteneur = row.conteneur;\n const repo = row.image_repo;\n const installed = installedVersions[conteneur] || row.version_installee;\n const statutObj = row.statut;\n const currentStatut = typeof statutObj === 'object' && statutObj !== null ? statutObj.value : statutObj;\n const criticiteObj = row.criticite;\n const criticite = typeof criticiteObj === 'object' && criticiteObj !== null ? criticiteObj.value : criticiteObj;\n\n items.push({\n json: {\n id: row.id,\n conteneur: conteneur,\n image_repo: repo,\n methode_verif: typeof row.methode_verif === 'object' && row.methode_verif !== null ? row.methode_verif.value : row.methode_verif,\n version_installee: installed,\n current_disponible: row.derniere_version_disponible,\n current_statut: currentStatut,\n criticite: criticite,\n notes: row.notes || ''\n }\n });\n}\nreturn items;\n" + }, + "id": "4", + "name": "Pr\u00e9parer \u00c9l\u00e9ments & Mapping", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 860, + 300 + ] + }, + { + "parameters": { + "url": "=https://hub.docker.com/v2/repositories/{{ $json.image_repo }}/tags?page_size=25&ordering=last_updated", + "options": {} + }, + "id": "5", + "name": "API Docker Hub", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1080, + 300 + ] + }, + { + "parameters": { + "jsCode": "\nconst origItem = $('Pr\u00e9parer \u00c9l\u00e9ments & Mapping').item.json;\nconst dockerHubData = $input.item.json;\nconst tagResults = dockerHubData.results || [];\nconst repo = origItem.image_repo;\nconst installed = String(origItem.version_installee || '');\nconst currentStatut = origItem.current_statut;\n\n// Helper: extract best stable tag\nlet candidateTags = [];\nfor (const t of tagResults) {\n const name = t.name;\n if (!name) continue;\n if (/(next|beta|dev|nightly|rc|alpha|test|latest|stable|edge|canary|master|main|^alpine$|^lts$)/i.test(name)) continue;\n if (/sha-[0-9a-f]+/i.test(name)) continue;\n \n if (repo === 'gitea/gitea') {\n if (!/^[0-9]+\\.[0-9]+\\.[0-9]+-rootless$/.test(name)) continue;\n } else if (repo === 'vaultwarden/server') {\n if (!/^[0-9]+\\.[0-9]+\\.[0-9]+-alpine$/.test(name)) continue;\n } else if (repo === 'portainer/portainer-ce') {\n if (!/^[0-9]+\\.[0-9]+\\.[0-9]+$/.test(name)) continue;\n } else if (['n8nio/n8n', 'crowdsecurity/crowdsec', 'tailscale/tailscale', 'baserow/baserow', 'triliumnext/trilium'].includes(repo)) {\n if (!/^[v]?[0-9]+\\.[0-9]+\\.[0-9]+$/.test(name)) continue;\n }\n\n candidateTags.push(name);\n}\n\nconst latestAvailable = candidateTags.length > 0 ? candidateTags[0] : (origItem.current_disponible || installed);\n\n// Normaliser pour comparaison\nlet cleanInstalled = installed.replace(/^v/, '');\nlet cleanAvailable = latestAvailable.replace(/^v/, '');\n\nlet isUpToDate = (installed === latestAvailable) || (cleanInstalled === cleanAvailable);\nif (installed.includes('image 14/05/2026')) {\n isUpToDate = false;\n}\n\nlet newStatut = '\u00c0 jour';\nlet ecart = '0';\n\nif (currentStatut === 'En pause') {\n newStatut = 'En pause';\n ecart = isUpToDate ? '0' : '\u00c0 confirmer';\n} else if (!isUpToDate) {\n newStatut = 'Retard mineur';\n ecart = 'Install\u00e9e: ' + installed + ' \u2794 Dispo: ' + latestAvailable;\n}\n\nconst today = new Date().toISOString().split('T')[0];\nconst shouldAlert = (newStatut !== '\u00c0 jour') && (newStatut !== 'En pause') && (currentStatut === '\u00c0 jour');\n\nreturn [{\n json: {\n id: origItem.id,\n conteneur: origItem.conteneur,\n image_repo: origItem.image_repo,\n version_installee: installed,\n derniere_version_disponible: latestAvailable,\n ecart: ecart,\n statut: newStatut,\n date_derniere_verification: today,\n criticite: origItem.criticite,\n should_alert: shouldAlert\n }\n}];\n" + }, + "id": "6", + "name": "Analyser \u00c9carts & Versions", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1300, + 300 + ] + }, + { + "parameters": { + "method": "PATCH", + "url": "=http://baserow:80/api/database/rows/table/1097/{{ $json.id }}/?user_field_names=true", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Host", + "value": "baserow.bolbol.tn" + }, + { + "name": "Authorization", + "value": "Token qS5KEYki5sA1VEdCn9hobXk0Bp4jd9fr" + } + ] + }, + "sendBody": true, + "bodyParameters": { + "parameters": [ + { + "name": "version_installee", + "value": "={{ $json.version_installee }}" + }, + { + "name": "derniere_version_disponible", + "value": "={{ $json.derniere_version_disponible }}" + }, + { + "name": "ecart", + "value": "={{ $json.ecart }}" + }, + { + "name": "statut", + "value": "={{ $json.statut }}" + }, + { + "name": "date_derniere_verification", + "value": "={{ $json.date_derniere_verification }}" + } + ] + }, + "options": {} + }, + "id": "7", + "name": "Mettre \u00e0 Jour Baserow", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1520, + 300 + ] + }, + { + "parameters": { + "jsCode": "\nconst allItems = $('Analyser \u00c9carts & Versions').all();\nconst alertItems = allItems.filter(i => i.json.should_alert || i.json.force_alert);\n\nif (alertItems.length === 0) {\n return [{ json: { no_alert: true, message: 'Aucun nouvel ecart' } }];\n}\n\nlet msg = '\ud83d\udd14 *Veille Versions Stack NAS \u2014 D\u00e9tection d\\'\u00e9carts*\\n\\n';\nfor (const item of alertItems) {\n const j = item.json;\n msg += '\u2022 *' + j.conteneur + '* (' + j.criticite + ') : `' + j.version_installee + '` \u2794 `' + j.derniere_version_disponible + '`\\n';\n}\nmsg += '\\n\ud83d\udccb *R\u00e9f\u00e9rentiel Baserow* : https://baserow.bolbol.tn/database/317/table/1097';\n\nreturn [{\n json: {\n message: msg,\n count: alertItems.length\n }\n}];\n" + }, + "id": "8", + "name": "Agr\u00e9ger Alertes", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1740, + 300 + ] + }, + { + "parameters": { + "chatId": "2084513684", + "text": "={{ $json.message }}", + "additionalFields": { + "parse_mode": "Markdown" + } + }, + "id": "9", + "name": "Telegram Notification", + "type": "n8n-nodes-base.telegram", + "typeVersion": 1.2, + "position": [ + 1960, + 300 + ], + "credentials": { + "telegramApi": { + "id": "OyOHh6jAzCDsiBux", + "name": "ND_Telegram" + } + }, + "webhookId": "55fcac45-4ff4-488c-ae3f-c7ff8d60ecd0" + }, + { + "parameters": {}, + "id": "manual-trigger-1", + "name": "When clicking Test workflow", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 200, + 140 + ] + }, + { + "parameters": { + "path": "test-veille-versions", + "responseMode": "lastNode", + "options": {} + }, + "id": "webhook-trigger-1", + "name": "Webhook Trigger Test", + "type": "n8n-nodes-base.webhook", + "typeVersion": 2, + "position": [ + 200, + 460 + ], + "webhookId": "test-veille-versions" + }, + { + "parameters": { + "conditions": { + "boolean": [ + { + "value1": "={{ $json.no_alert }}", + "value2": true + } + ] + } + }, + "id": "filter-node-1", + "name": "Filtrer Alertes", + "type": "n8n-nodes-base.if", + "typeVersion": 1, + "position": [ + 1850, + 300 + ] + } + ], + "connections": { + "Schedule Trigger (Lundi 08h00)": { + "main": [ + [ + { + "node": "Lire Table Baserow", + "type": "main", + "index": 0 + } + ] + ] + }, + "Lire Table Baserow": { + "main": [ + [ + { + "node": "SSH Versions NAS (Restreint)", + "type": "main", + "index": 0 + } + ] + ] + }, + "SSH Versions NAS (Restreint)": { + "main": [ + [ + { + "node": "Pr\u00e9parer \u00c9l\u00e9ments & Mapping", + "type": "main", + "index": 0 + } + ] + ] + }, + "Pr\u00e9parer \u00c9l\u00e9ments & Mapping": { + "main": [ + [ + { + "node": "API Docker Hub", + "type": "main", + "index": 0 + } + ] + ] + }, + "API Docker Hub": { + "main": [ + [ + { + "node": "Analyser \u00c9carts & Versions", + "type": "main", + "index": 0 + } + ] + ] + }, + "Analyser \u00c9carts & Versions": { + "main": [ + [ + { + "node": "Mettre \u00e0 Jour Baserow", + "type": "main", + "index": 0 + } + ] + ] + }, + "Mettre \u00e0 Jour Baserow": { + "main": [ + [ + { + "node": "Agr\u00e9ger Alertes", + "type": "main", + "index": 0 + } + ] + ] + }, + "Agr\u00e9ger Alertes": { + "main": [ + [ + { + "node": "Filtrer Alertes", + "type": "main", + "index": 0 + } + ] + ] + }, + "When clicking Test workflow": { + "main": [ + [ + { + "node": "Lire Table Baserow", + "type": "main", + "index": 0 + } + ] + ] + }, + "Webhook Trigger Test": { + "main": [ + [ + { + "node": "Lire Table Baserow", + "type": "main", + "index": 0 + } + ] + ] + }, + "Filtrer Alertes": { + "main": [ + [], + [ + { + "node": "Telegram Notification", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "nodeGroups": [], + "settings": { + "executionOrder": "v1", + "timezone": "Africa/Tunis" + }, + "staticData": { + "node:Schedule Trigger (Lundi 08h00)": { + "recurrenceRules": [ + null + ], + "recurrenceRuleSignatures": [ + null + ] + } + }, + "pinData": null, + "meta": null, + "tags": [], + "shared": [ + { + "role": "workflow:owner", + "workflowId": "zUWN5K4Q1A2JKFIS", + "projectId": "3IcTI301RXGOgKWv", + "project": { + "id": "3IcTI301RXGOgKWv", + "name": "Nabil Derouiche ", + "type": "personal", + "icon": null, + "description": null, + "customTelemetryTags": [], + "creatorId": "809184e1-fa99-4ef9-95d5-047b8b7c225e", + "createdAt": "2025-11-22T16:04:26.142Z", + "updatedAt": "2025-11-22T17:22:23.186Z" + }, + "createdAt": "2026-08-21T19:10:15.587Z", + "updatedAt": "2026-08-21T19:10:15.587Z" + } + ], + "activeVersion": { + "versionId": "c2da509a-651c-4fa9-9aa8-00f4ab440123", + "workflowId": "zUWN5K4Q1A2JKFIS", + "nodes": [ + { + "parameters": { + "rule": { + "interval": [ + { + "field": "weeks", + "triggerAtHour": 8, + "triggerAtMinute": 0 + } + ] + } + }, + "id": "1", + "name": "Schedule Trigger (Lundi 08h00)", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.2, + "position": [ + 200, + 300 + ] + }, + { + "parameters": { + "url": "http://baserow:80/api/database/rows/table/1097/?user_field_names=true&size=100", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Host", + "value": "baserow.bolbol.tn" + }, + { + "name": "Authorization", + "value": "Token qS5KEYki5sA1VEdCn9hobXk0Bp4jd9fr" + } + ] + }, + "options": {} + }, + "id": "2", + "name": "Lire Table Baserow", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 420, + 300 + ] + }, + { + "parameters": { + "command": "all", + "authentication": "privateKey" + }, + "id": "3", + "name": "SSH Versions NAS (Restreint)", + "type": "n8n-nodes-base.ssh", + "typeVersion": 1, + "position": [ + 640, + 300 + ], + "credentials": { + "sshPrivateKey": { + "id": "UjDRfssNSG2A9Ku9", + "name": "SSH n8n-versions-ro" + } + } + }, + { + "parameters": { + "jsCode": "\nconst baserowRows = $('Lire Table Baserow').first().json.results;\nconst sshOutput = $input.first().json.stdout;\nlet installedVersions = {};\ntry {\n installedVersions = JSON.parse(sshOutput);\n} catch (e) {\n // fallback parsing\n installedVersions = {};\n}\n\nconst items = [];\nfor (const row of baserowRows) {\n const conteneur = row.conteneur;\n const repo = row.image_repo;\n const installed = installedVersions[conteneur] || row.version_installee;\n const statutObj = row.statut;\n const currentStatut = typeof statutObj === 'object' && statutObj !== null ? statutObj.value : statutObj;\n const criticiteObj = row.criticite;\n const criticite = typeof criticiteObj === 'object' && criticiteObj !== null ? criticiteObj.value : criticiteObj;\n\n items.push({\n json: {\n id: row.id,\n conteneur: conteneur,\n image_repo: repo,\n methode_verif: typeof row.methode_verif === 'object' && row.methode_verif !== null ? row.methode_verif.value : row.methode_verif,\n version_installee: installed,\n current_disponible: row.derniere_version_disponible,\n current_statut: currentStatut,\n criticite: criticite,\n notes: row.notes || ''\n }\n });\n}\nreturn items;\n" + }, + "id": "4", + "name": "Pr\u00e9parer \u00c9l\u00e9ments & Mapping", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 860, + 300 + ] + }, + { + "parameters": { + "url": "=https://hub.docker.com/v2/repositories/{{ $json.image_repo }}/tags?page_size=25&ordering=last_updated", + "options": {} + }, + "id": "5", + "name": "API Docker Hub", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1080, + 300 + ] + }, + { + "parameters": { + "jsCode": "\nconst origItem = $('Pr\u00e9parer \u00c9l\u00e9ments & Mapping').item.json;\nconst dockerHubData = $input.item.json;\nconst tagResults = dockerHubData.results || [];\nconst repo = origItem.image_repo;\nconst installed = String(origItem.version_installee || '');\nconst currentStatut = origItem.current_statut;\n\n// Helper: extract best stable tag\nlet candidateTags = [];\nfor (const t of tagResults) {\n const name = t.name;\n if (!name) continue;\n if (/(next|beta|dev|nightly|rc|alpha|test|latest|stable|edge|canary|master|main|^alpine$|^lts$)/i.test(name)) continue;\n if (/sha-[0-9a-f]+/i.test(name)) continue;\n \n if (repo === 'gitea/gitea') {\n if (!/^[0-9]+\\.[0-9]+\\.[0-9]+-rootless$/.test(name)) continue;\n } else if (repo === 'vaultwarden/server') {\n if (!/^[0-9]+\\.[0-9]+\\.[0-9]+-alpine$/.test(name)) continue;\n } else if (repo === 'portainer/portainer-ce') {\n if (!/^[0-9]+\\.[0-9]+\\.[0-9]+$/.test(name)) continue;\n } else if (['n8nio/n8n', 'crowdsecurity/crowdsec', 'tailscale/tailscale', 'baserow/baserow', 'triliumnext/trilium'].includes(repo)) {\n if (!/^[v]?[0-9]+\\.[0-9]+\\.[0-9]+$/.test(name)) continue;\n }\n\n candidateTags.push(name);\n}\n\nconst latestAvailable = candidateTags.length > 0 ? candidateTags[0] : (origItem.current_disponible || installed);\n\n// Normaliser pour comparaison\nlet cleanInstalled = installed.replace(/^v/, '');\nlet cleanAvailable = latestAvailable.replace(/^v/, '');\n\nlet isUpToDate = (installed === latestAvailable) || (cleanInstalled === cleanAvailable);\nif (installed.includes('image 14/05/2026')) {\n isUpToDate = false;\n}\n\nlet newStatut = '\u00c0 jour';\nlet ecart = '0';\n\nif (currentStatut === 'En pause') {\n newStatut = 'En pause';\n ecart = isUpToDate ? '0' : '\u00c0 confirmer';\n} else if (!isUpToDate) {\n newStatut = 'Retard mineur';\n ecart = 'Install\u00e9e: ' + installed + ' \u2794 Dispo: ' + latestAvailable;\n}\n\nconst today = new Date().toISOString().split('T')[0];\nconst shouldAlert = (newStatut !== '\u00c0 jour') && (newStatut !== 'En pause') && (currentStatut === '\u00c0 jour');\n\nreturn [{\n json: {\n id: origItem.id,\n conteneur: origItem.conteneur,\n image_repo: origItem.image_repo,\n version_installee: installed,\n derniere_version_disponible: latestAvailable,\n ecart: ecart,\n statut: newStatut,\n date_derniere_verification: today,\n criticite: origItem.criticite,\n should_alert: shouldAlert\n }\n}];\n" + }, + "id": "6", + "name": "Analyser \u00c9carts & Versions", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1300, + 300 + ] + }, + { + "parameters": { + "method": "PATCH", + "url": "=http://baserow:80/api/database/rows/table/1097/{{ $json.id }}/?user_field_names=true", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Host", + "value": "baserow.bolbol.tn" + }, + { + "name": "Authorization", + "value": "Token qS5KEYki5sA1VEdCn9hobXk0Bp4jd9fr" + } + ] + }, + "sendBody": true, + "bodyParameters": { + "parameters": [ + { + "name": "version_installee", + "value": "={{ $json.version_installee }}" + }, + { + "name": "derniere_version_disponible", + "value": "={{ $json.derniere_version_disponible }}" + }, + { + "name": "ecart", + "value": "={{ $json.ecart }}" + }, + { + "name": "statut", + "value": "={{ $json.statut }}" + }, + { + "name": "date_derniere_verification", + "value": "={{ $json.date_derniere_verification }}" + } + ] + }, + "options": {} + }, + "id": "7", + "name": "Mettre \u00e0 Jour Baserow", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1520, + 300 + ] + }, + { + "parameters": { + "jsCode": "\nconst allItems = $('Analyser \u00c9carts & Versions').all();\nconst alertItems = allItems.filter(i => i.json.should_alert || i.json.force_alert);\n\nif (alertItems.length === 0) {\n return [{ json: { no_alert: true, message: 'Aucun nouvel ecart' } }];\n}\n\nlet msg = '\ud83d\udd14 *Veille Versions Stack NAS \u2014 D\u00e9tection d\\'\u00e9carts*\\n\\n';\nfor (const item of alertItems) {\n const j = item.json;\n msg += '\u2022 *' + j.conteneur + '* (' + j.criticite + ') : `' + j.version_installee + '` \u2794 `' + j.derniere_version_disponible + '`\\n';\n}\nmsg += '\\n\ud83d\udccb *R\u00e9f\u00e9rentiel Baserow* : https://baserow.bolbol.tn/database/317/table/1097';\n\nreturn [{\n json: {\n message: msg,\n count: alertItems.length\n }\n}];\n" + }, + "id": "8", + "name": "Agr\u00e9ger Alertes", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1740, + 300 + ] + }, + { + "parameters": { + "chatId": "2084513684", + "text": "={{ $json.message }}", + "additionalFields": { + "parse_mode": "Markdown" + } + }, + "id": "9", + "name": "Telegram Notification", + "type": "n8n-nodes-base.telegram", + "typeVersion": 1.2, + "position": [ + 1960, + 300 + ], + "credentials": { + "telegramApi": { + "id": "OyOHh6jAzCDsiBux", + "name": "ND_Telegram" + } + }, + "webhookId": "55fcac45-4ff4-488c-ae3f-c7ff8d60ecd0" + }, + { + "parameters": {}, + "id": "manual-trigger-1", + "name": "When clicking Test workflow", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 200, + 140 + ] + }, + { + "parameters": { + "path": "test-veille-versions", + "responseMode": "lastNode", + "options": {} + }, + "id": "webhook-trigger-1", + "name": "Webhook Trigger Test", + "type": "n8n-nodes-base.webhook", + "typeVersion": 2, + "position": [ + 200, + 460 + ], + "webhookId": "test-veille-versions" + }, + { + "parameters": { + "conditions": { + "boolean": [ + { + "value1": "={{ $json.no_alert }}", + "value2": true + } + ] + } + }, + "id": "filter-node-1", + "name": "Filtrer Alertes", + "type": "n8n-nodes-base.if", + "typeVersion": 1, + "position": [ + 1850, + 300 + ] + } + ], + "connections": { + "Schedule Trigger (Lundi 08h00)": { + "main": [ + [ + { + "node": "Lire Table Baserow", + "type": "main", + "index": 0 + } + ] + ] + }, + "Lire Table Baserow": { + "main": [ + [ + { + "node": "SSH Versions NAS (Restreint)", + "type": "main", + "index": 0 + } + ] + ] + }, + "SSH Versions NAS (Restreint)": { + "main": [ + [ + { + "node": "Pr\u00e9parer \u00c9l\u00e9ments & Mapping", + "type": "main", + "index": 0 + } + ] + ] + }, + "Pr\u00e9parer \u00c9l\u00e9ments & Mapping": { + "main": [ + [ + { + "node": "API Docker Hub", + "type": "main", + "index": 0 + } + ] + ] + }, + "API Docker Hub": { + "main": [ + [ + { + "node": "Analyser \u00c9carts & Versions", + "type": "main", + "index": 0 + } + ] + ] + }, + "Analyser \u00c9carts & Versions": { + "main": [ + [ + { + "node": "Mettre \u00e0 Jour Baserow", + "type": "main", + "index": 0 + } + ] + ] + }, + "Mettre \u00e0 Jour Baserow": { + "main": [ + [ + { + "node": "Agr\u00e9ger Alertes", + "type": "main", + "index": 0 + } + ] + ] + }, + "Agr\u00e9ger Alertes": { + "main": [ + [ + { + "node": "Filtrer Alertes", + "type": "main", + "index": 0 + } + ] + ] + }, + "When clicking Test workflow": { + "main": [ + [ + { + "node": "Lire Table Baserow", + "type": "main", + "index": 0 + } + ] + ] + }, + "Webhook Trigger Test": { + "main": [ + [ + { + "node": "Lire Table Baserow", + "type": "main", + "index": 0 + } + ] + ] + }, + "Filtrer Alertes": { + "main": [ + [], + [ + { + "node": "Telegram Notification", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "nodeGroups": [], + "authors": "Nabil Derouiche", + "name": null, + "description": null, + "autosaved": false, + "createdAt": "2026-08-21T19:16:24.469Z", + "updatedAt": "2026-08-21T19:16:24.469Z", + "workflowPublishHistory": [ + { + "id": 337, + "workflowId": "zUWN5K4Q1A2JKFIS", + "versionId": "c2da509a-651c-4fa9-9aa8-00f4ab440123", + "event": "activated", + "userId": "809184e1-fa99-4ef9-95d5-047b8b7c225e", + "createdAt": "2026-08-21T19:16:30.446Z" + }, + { + "id": 338, + "workflowId": "zUWN5K4Q1A2JKFIS", + "versionId": "c2da509a-651c-4fa9-9aa8-00f4ab440123", + "event": "deactivated", + "userId": "809184e1-fa99-4ef9-95d5-047b8b7c225e", + "createdAt": "2026-08-21T19:16:30.834Z" + }, + { + "id": 339, + "workflowId": "zUWN5K4Q1A2JKFIS", + "versionId": "c2da509a-651c-4fa9-9aa8-00f4ab440123", + "event": "activated", + "userId": "809184e1-fa99-4ef9-95d5-047b8b7c225e", + "createdAt": "2026-08-21T19:16:31.044Z" + } + ] + } +}