diff --git a/common/dsh-vps-procedure.md b/common/dsh-vps-procedure.md new file mode 100644 index 0000000..e9f9966 --- /dev/null +++ b/common/dsh-vps-procedure.md @@ -0,0 +1,85 @@ +# DeepSeek Harness (DSH) VPS & FileBrowser — Guide d'Exploitation et MAJ + +Ce document décrit la structure, la configuration et la procédure de mise à jour de la stack **DeepSeek Harness (`dsh-vps`)** et de son compagnon **FileBrowser (`dsh-vps-filebrowser`)** hébergés sur le VPS Contabo (`100.94.90.119`). + +--- + +## 1. Architecture et Composants + +``` +/home/dsh-agent/dsh-vps/ +├── Dockerfile # node:20-bookworm-slim + @deepseek-ai/dsh pinné + inkstone + patch 0.0.0.0 +├── docker-compose.yml # Services: dsh-proxy, dsh-vps, dsh-filebrowser +├── entrypoint.sh # Script de démarrage dsh-web-app +├── nginx.conf # Proxy inverse TLS + Basic Auth (ports 8900 et 8901) +├── auth/ # .htpasswd, credentials.txt +├── certs/ # Certificats auto-signés TLS +└── backups/ # Sauvegardes dsh_vps_home +``` + +### Services et Ports +| Service | Conteneur | Port Interne | Port Public / Tailscale | Auth | Description | +|---|---|---|---|---|---| +| `dsh-proxy` | `dsh-vps-proxy` | 8900 / 8901 | `100.94.90.119:8900` / `8901` | Basic Auth | Proxy TLS Nginx unifié | +| `dsh-vps` | `dsh-vps` | 3080 | Réseau Docker `dsh_vps_net` | Proxy 8900 | Instance DSH Web UI & Agent | +| `dsh-filebrowser` | `dsh-vps-filebrowser` | 8080 | Réseau Docker `dsh_vps_net` | Proxy 8901 | Explorateur de fichiers workspace | + +--- + +## 2. Points d'Attention & Pièges Techniques + +1. **Version DSH dans Dockerfile** : + - La version est explicitement fixée dans le `Dockerfile` à la ligne `RUN npm install -g @deepseek-ai/dsh@ @banlan/inkstone`. + - Ne jamais utiliser `@latest` sans vérifier les breaking changes. + +2. **Patch de binding `0.0.0.0` dans `startup.js`** : + - Par défaut, `@deepseek-ai/dsh-web-app` interdit l'écoute sur `0.0.0.0` (erreur de sécurité native). + - Dans le conteneur, l'écoute sur `0.0.0.0` est indispensable pour que le proxy Nginx (`dsh-vps-proxy`) puisse lui transférer les requêtes. + - Le `Dockerfile` applique un patch obligatoire : + ```dockerfile + RUN find /usr/local/lib/node_modules -name "startup.js" -path "*/@deepseek-ai/dsh-web-app/*" -exec sed -i 's/if (options.host === "0.0.0.0")/if (false \&\& options.host === "0.0.0.0")/' {} + + ``` + - **Vérification post-build impérative** : + ```bash + docker exec dsh-vps sh -c 'grep -n "options.host" $(find /usr/local/lib/node_modules -name startup.js -path "*/@deepseek-ai/dsh-web-app/*")' + ``` + +3. **FileBrowser Entrypoint (`v2.63.23+`)** : + - Depuis les versions récentes de l'image `filebrowser/filebrowser`, le binaire est localisé dans `/bin/filebrowser` (et non `/filebrowser`). + - L'entrypoint officiel utilise `tini` et `/init.sh` qui tente d'écrire dans `/config` appartenant à `root`. + - L'exécution avec `user: "1001:1001"` requiert de surcharger l'entrypoint : + ```yaml + entrypoint: ["/bin/filebrowser"] + command: ["--noauth", "-a", "0.0.0.0", "-p", "8080", "-r", "/srv", "-d", "/srv/.filebrowser.db"] + ``` + +--- + +## 3. Procédure de Mise à Jour DSH + +1. **Sauvegarde préalable de `dsh_vps_home`** : + ```bash + ssh -i ~/.ssh/vps_dsh_agent dsh-agent@100.94.90.119 \ + "docker run --rm -v dsh_vps_home:/source:ro -v /home/dsh-agent/dsh-vps/backups:/backup alpine \ + tar -czf /backup/dsh_vps_home-$(date +%Y%m%d-%H%M%S).tar.gz -C /source ." + ``` + +2. **Mise à jour du Dockerfile** : + Modifier le tag de version dans `/home/dsh-agent/dsh-vps/Dockerfile`. + +3. **Rebuild sans cache** : + ```bash + cd /home/dsh-agent/dsh-vps && docker compose build --no-cache dsh-vps + ``` + +4. **Recréation du conteneur** : + ```bash + docker compose up -d --force-recreate dsh-vps + ``` + +5. **Contrôles de conformité** : + - Version binaire : `docker exec dsh-vps dsh --version` + - Patch actif : `docker exec dsh-vps sh -c 'grep -n "options.host" $(find /usr/local/lib/node_modules -name startup.js -path "*/@deepseek-ai/dsh-web-app/*")'` + - Écoute interne : `docker exec dsh-vps curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3080` + - Proxy HTTPS : `curl -sk -u "dsh-admin:..." https://100.94.90.119:8900` + - Vérification Bifrost : `docker exec dsh-vps curl -s -H "Authorization: Bearer sk-bf-..." http://100.86.197.88:3086/v1/models` diff --git a/common/veille-versions-stack.md b/common/veille-versions-stack.md index a0d1a8b..17e88ee 100644 --- a/common/veille-versions-stack.md +++ b/common/veille-versions-stack.md @@ -1,6 +1,6 @@ -# Veille Versions Stack NAS & Référentiel Baserow +# Veille Versions Stack NAS & VPS — 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. +Ce document décrit le fonctionnement du dispositif de suivi et de veille automatisée des versions des conteneurs du NAS Synology et du VPS Contabo. > **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. @@ -11,10 +11,11 @@ Ce document décrit le fonctionnement du dispositif de suivi et de veille automa ```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"] + ReadBR --> SSHNAS["SSH Restreint NAS (n8n-versions-ro @ 192.168.100.33:22222)"] + SSHNAS --> SSHVPS["SSH Restreint VPS (n8n-versions-ro @ tailscale-nyora-bridge:22223)"] + SSHVPS --> MapItems["Préparer Mapping & Éléments (Fusion NAS & VPS)"] + MapItems --> Registries["API Registres (Docker Hub & npm registry)"] + Registries --> Analyze["Analyser Écarts & Versions (Boucle 11 items)"] 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)"] @@ -26,16 +27,17 @@ graph TD ## 2. Composants Techniques ### A. Référentiel Baserow (`infra_versions`) -- **Workspace** : `187 (Perso)` *(déplacé depuis le 148 le 21/08/2026)* +- **Workspace** : `187 (Perso)` - **Database** : `Infra Stack` (ID `317`) - **Table** : `infra_versions` (ID `1097`) - **URL** : `https://baserow.bolbol.tn/database/317/table/1097` - **Token API dédié** : `n8n-infra-versions` (Token ID `32`, scopé Workspace 187 Perso) - **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 + - `image_repo` (Texte) : Repository Docker Hub ou nom de package npm (ex: `@deepseek-ai/dsh`) + - `methode_verif` (Single select) : `image_tag`, `exec_version` ou `npm_registry` + - `emplacement` (Single select) : `NAS` ou `VPS` + - `version_installee` (Texte) : Version réelle active sur l'hôte - `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` @@ -48,20 +50,34 @@ graph TD --- ### 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 + command="/volume1/docker/scripts/versions-check.sh",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKDwuAwKnKiKskL+Vr4WYhn/uySlk5jyOHPh1R/P/9K0 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`. +- **Script exécutable** : `/volume1/docker/scripts/versions-check.sh` (mode 755) --- -### C. Workflow n8n (`veille-versions-stack`) +### C. Accès SSH Restreint VPS (`n8n-versions-ro`) +- **Compte VPS** : `n8n-versions-ro` (UID `1002`, GID `1002`, membre du groupe `docker`). +- **Clé SSH dédiée** : `/volume1/docker/scripts/id_ed25519_n8n_versions_vps` + - **Fingerprint réel** : `256 SHA256:fTDnqFouAhwApAsnQjuLoGjvZCNoHAwYC4gpGqRvygY n8n-versions-ro@vps (ED25519)` + - **Clé publique** : `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFdwCzUBTsBKNSRxhjwU82o9T2VbQn4HKbp+3mFyIQTq n8n-versions-ro@vps` +- **Restriction `authorized_keys` VPS** (`/home/n8n-versions-ro/.ssh/authorized_keys`) : + ```text + command="/home/n8n-versions-ro/versions-check-vps.sh",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFdwCzUBTsBKNSRxhjwU82o9T2VbQn4HKbp+3mFyIQTq n8n-versions-ro@vps + ``` +- **Script exécutable VPS** : `/home/n8n-versions-ro/versions-check-vps.sh` (mode 755) +- **Relais Réseau n8n ➔ VPS** : + - OpenSSH standard écoute sur le port `22222` sur l'interface `tailscale0` du VPS (`100.94.90.119:22222`). + - Sur le NAS, le conteneur `tailscale-vps-ssh-relay` (partageant l'espace réseau de `tailscale-nyora-bridge`) relaie le port `22223` vers `100.94.90.119:22222`. + - Le conteneur `n8n` interroge directement `tailscale-nyora-bridge:22223`. + +--- + +### D. 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`. @@ -69,32 +85,38 @@ Pour respecter le principe de moindre privilège et interdire tout shell interac --- -## 3. Table des 9 Conteneurs Suivis +## 3. Table des 11 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) | +| Conteneur | Emplacement | Image Repo / Package | Méthode | Criticité | Particularités | +|---|---|---|---|---|---| +| `n8n` | NAS | `n8nio/n8n` | `exec_version` | Important | Image custom (docx, exceljs) | +| `bifrost` | NAS | `maximhq/bifrost` | `image_tag` | Critique | Pin image, backup config.db/config.json | +| `gitea` | NAS | `gitea/gitea` | `image_tag` | Important | Pin rootless, backup natif `gitea dump` | +| `portainer` | NAS | `portainer/portainer-ce` | `image_tag` | Mineur | Pilote stacks, MAJ en priorité | +| `vaultwarden` | NAS | `vaultwarden/server` | `image_tag` | Mineur | Pin alpine, backup db.sqlite3 | +| `baserow` | NAS | `baserow/baserow` | `exec_version` | Important | Mode minimal, DNS dynamique Nginx | +| `crowdsec` | NAS | `crowdsecurity/crowdsec` | `exec_version` | Important | Sécurité IDS/IPS DSM | +| `tailscale-nyora-bridge` | NAS | `tailscale/tailscale` | `image_tag` | Mineur | Bridge HTTP/SOCKS5 | +| `trilium` | NAS | `triliumnext/trilium` | `image_tag` | Mineur | `En pause` (exclu des alertes actives) | +| `dsh-vps` | VPS | `@deepseek-ai/dsh` | `npm_registry` | Important | Build custom, version npm pinnée, patch binding `startup.js` | +| `dsh-vps-filebrowser` | VPS | `filebrowser/filebrowser` | `image_tag` | Mineur | Compagnon Web UI, entrypoint `/bin/filebrowser` | --- ## 4. Procédure de Test et Validation -1. **Test d'exécution manuelle sans alerte (tout à jour)** : +1. **Test d'exécution manuelle globale via webhook** : ```bash - curl -s http://192.168.100.33:5678/webhook/test-veille-versions - # Réponse attendue : {"no_alert": true, "message": "Aucun nouvel ecart"} + curl -s http://192.168.100.33:5678/webhook/test-veille-versions -H "Host: n8n.bolbol.tn" + # Réponse attendue si tout est à jour : {"no_alert": true, "message": "Aucun nouvel ecart"} ``` -2. **Test d'interrogation SSH unitaire** : +2. **Test d'interrogation SSH unitaire NAS** : ```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) + ssh -p 22222 -i /volume1/docker/scripts/id_ed25519_n8n_versions -o BatchMode=yes n8n-versions-ro@127.0.0.1 "all" + ``` + +3. **Test d'interrogation SSH unitaire VPS** (depuis le conteneur `n8n`) : + ```bash + docker exec n8n ssh -p 22223 -i /tmp/id_vps_n8n -o BatchMode=yes n8n-versions-ro@tailscale-nyora-bridge "all" ``` diff --git a/common/workflows/veille-versions-stack.n8n.json b/common/workflows/veille-versions-stack.n8n.json index fde461f..5e974f6 100644 --- a/common/workflows/veille-versions-stack.n8n.json +++ b/common/workflows/veille-versions-stack.n8n.json @@ -1,831 +1,431 @@ { - "id": "zUWN5K4Q1A2JKFIS", - "name": "veille-versions-stack", - "description": null, - "active": true, - "activeVersionId": "4e4291da-fa65-4d62-bccc-c83dcf0b3944", - "createdAt": "2026-08-21T19:10:15.587Z", - "updatedAt": "2026-08-21T19:59:21.024Z", - "isArchived": false, - "versionId": "4e4291da-fa65-4d62-bccc-c83dcf0b3944", - "versionCounter": 9, - "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-oauth-proxy: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" - } + "name": "veille-versions-stack", + "nodes": [ + { + "parameters": { + "rule": { + "interval": [ + { + "field": "weeks", + "triggerAtDay": [ + 1 + ], + "triggerAtHour": 8 } - }, - { - "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-oauth-proxy: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 - } - ] - ] + ] } + }, + "id": "1", + "name": "Schedule Trigger (Lundi 08h00)", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.2, + "position": [ + 200, + 300 + ] }, - "nodeGroups": [], - "settings": { - "executionOrder": "v1", - "timezone": "Africa/Tunis" + { + "parameters": {}, + "id": "manual-trigger-1", + "name": "When clicking Test workflow", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 200, + 140 + ] }, - "staticData": { - "node:Schedule Trigger (Lundi 08h00)": { - "recurrenceRules": [ - null - ], - "recurrenceRuleSignatures": [ - null - ] - } + { + "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" }, - "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": "4e4291da-fa65-4d62-bccc-c83dcf0b3944", - "workflowId": "zUWN5K4Q1A2JKFIS", - "nodes": [ + { + "parameters": { + "url": "http://baserow-oauth-proxy:80/api/database/rows/table/1097/?user_field_names=true&size=100", + "sendHeaders": true, + "headerParameters": { + "parameters": [ { - "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 - ] + "name": "Host", + "value": "baserow.bolbol.tn" }, { - "parameters": { - "url": "http://baserow-oauth-proxy: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-oauth-proxy: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 - } - ] - ] + "name": "Authorization", + "value": "Token qS5KEYki5sA1VEdCn9hobXk0Bp4jd9fr" } + ] }, - "nodeGroups": [], - "authors": "Nabil Derouiche", - "name": null, - "description": null, - "autosaved": false, - "createdAt": "2026-08-21T19:59:21.027Z", - "updatedAt": "2026-08-21T19:59:21.027Z", - "workflowPublishHistory": [ + "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": { + "command": "all", + "authentication": "privateKey" + }, + "id": "ssh-vps-node", + "name": "SSH Versions VPS (Restreint)", + "type": "n8n-nodes-base.ssh", + "typeVersion": 1, + "position": [ + 860, + 300 + ], + "credentials": { + "sshPrivateKey": { + "id": "VpsVersionsSsh01", + "name": "SSH n8n-versions-ro-vps" + } + } + }, + { + "parameters": { + "jsCode": "\nconst baserowRows = $('Lire Table Baserow').first().json.results;\nconst sshNasRaw = $('SSH Versions NAS (Restreint)').first()?.json?.stdout || '';\nconst sshVpsRaw = $('SSH Versions VPS (Restreint)').first()?.json?.stdout || '';\n\nlet installedVersions = {};\ntry {\n Object.assign(installedVersions, JSON.parse(sshNasRaw));\n} catch (e) {}\n\ntry {\n Object.assign(installedVersions, JSON.parse(sshVpsRaw));\n} catch (e) {}\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 const methode = typeof row.methode_verif === 'object' && row.methode_verif !== null ? row.methode_verif.value : row.methode_verif;\n const emplacement = typeof row.emplacement === 'object' && row.emplacement !== null ? row.emplacement.value : (row.emplacement || 'NAS');\n\n let apiUrl = '';\n if (methode === 'npm_registry') {\n apiUrl = 'https://registry.npmjs.org/' + encodeURIComponent(repo);\n } else {\n apiUrl = 'https://hub.docker.com/v2/repositories/' + repo + '/tags?page_size=25&ordering=last_updated';\n }\n\n items.push({\n json: {\n id: row.id,\n conteneur: conteneur,\n image_repo: repo,\n methode_verif: methode,\n emplacement: emplacement,\n api_url: apiUrl,\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éparer Éléments & Mapping", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1080, + 300 + ] + }, + { + "parameters": { + "url": "={{ $json.api_url }}", + "sendHeaders": true, + "headerParameters": { + "parameters": [ { - "id": 345, - "workflowId": "zUWN5K4Q1A2JKFIS", - "versionId": "4e4291da-fa65-4d62-bccc-c83dcf0b3944", - "event": "activated", - "userId": "809184e1-fa99-4ef9-95d5-047b8b7c225e", - "createdAt": "2026-08-21T19:59:21.919Z" - }, - { - "id": 346, - "workflowId": "zUWN5K4Q1A2JKFIS", - "versionId": "4e4291da-fa65-4d62-bccc-c83dcf0b3944", - "event": "deactivated", - "userId": "809184e1-fa99-4ef9-95d5-047b8b7c225e", - "createdAt": "2026-08-21T19:59:22.240Z" - }, - { - "id": 347, - "workflowId": "zUWN5K4Q1A2JKFIS", - "versionId": "4e4291da-fa65-4d62-bccc-c83dcf0b3944", - "event": "activated", - "userId": "809184e1-fa99-4ef9-95d5-047b8b7c225e", - "createdAt": "2026-08-21T19:59:22.428Z" + "name": "User-Agent", + "value": "n8n-veille-versions/1.0" } - ] + ] + }, + "options": {} + }, + "onError": "continueRegularOutput", + "retryOnFail": true, + "maxTries": 3, + "waitBetweenTries": 2000, + "id": "5", + "name": "API Registres (Docker Hub / npm)", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1300, + 300 + ] + }, + { + "parameters": { + "jsCode": "\nconst origItems = $('Préparer Éléments & Mapping').all();\nconst registryItems = $input.all();\nconst results = [];\n\nfor (let i = 0; i < origItems.length; i++) {\n const origItem = origItems[i].json;\n const registryData = registryItems[i]?.json || {};\n const repo = origItem.image_repo;\n const methode = origItem.methode_verif;\n const installed = String(origItem.version_installee || '');\n const currentStatut = origItem.current_statut;\n\n let latestAvailable = '';\n\n if (methode === 'npm_registry') {\n const distTags = registryData['dist-tags'] || {};\n latestAvailable = distTags.latest || distTags.next || '';\n if (!latestAvailable && registryData.versions) {\n const versions = Object.keys(registryData.versions);\n latestAvailable = versions[versions.length - 1] || '';\n }\n } else {\n const tagResults = registryData.results || [];\n let candidateTags = [];\n for (const t of tagResults) {\n const name = t.name;\n if (!name) continue;\n if (/(beta|dev|nightly|alpha|test|latest|stable|edge|canary|master|main|^alpine$|^lts$|-s6$)/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 (repo === 'filebrowser/filebrowser') {\n if (!/^v?[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 latestAvailable = candidateTags.length > 0 ? candidateTags[0] : (origItem.current_disponible || installed);\n }\n\n if (!latestAvailable) {\n latestAvailable = origItem.current_disponible || installed;\n }\n\n let cleanInstalled = installed.replace(/^v/, '');\n let cleanAvailable = latestAvailable.replace(/^v/, '');\n\n let isUpToDate = (installed === latestAvailable) || (cleanInstalled === cleanAvailable);\n\n let newStatut = 'À jour';\n let ecart = '0';\n\n if (currentStatut === 'En pause') {\n newStatut = 'En pause';\n ecart = isUpToDate ? '0' : 'À confirmer';\n } else if (!isUpToDate) {\n newStatut = 'Retard mineur';\n ecart = 'Installée: ' + installed + ' ➔ Dispo: ' + latestAvailable;\n }\n\n const today = new Date().toISOString().split('T')[0];\n const shouldAlert = (newStatut !== 'À jour') && (newStatut !== 'En pause') && (currentStatut === 'À jour');\n\n results.push({\n json: {\n id: origItem.id,\n conteneur: origItem.conteneur,\n image_repo: origItem.image_repo,\n emplacement: origItem.emplacement,\n methode_verif: origItem.methode_verif,\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}\n\nreturn results;\n" + }, + "id": "6", + "name": "Analyser Écarts & Versions", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1520, + 300 + ] + }, + { + "parameters": { + "method": "PATCH", + "url": "=http://baserow-oauth-proxy: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" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "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": {} + }, + "onError": "continueRegularOutput", + "id": "7", + "name": "Mettre à Jour Baserow", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1740, + 300 + ] + }, + { + "parameters": { + "jsCode": "\nconst allItems = $('Analyser Écarts & 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 = '🔔 *Veille Versions Stack (NAS & VPS) — Détection d\\'écarts*\\n\\n';\nfor (const item of alertItems) {\n const j = item.json;\n msg += '• *' + j.conteneur + '* [' + j.emplacement + '] (' + j.criticite + ') : `' + j.version_installee + '` ➔ `' + j.derniere_version_disponible + '`\\n';\n}\nmsg += '\\n📋 *Référentiel 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éger Alertes", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1960, + 300 + ] + }, + { + "parameters": { + "conditions": { + "boolean": [ + { + "value1": "={{ $json.no_alert }}", + "value2": true + } + ] + } + }, + "id": "filter-node-1", + "name": "Filtrer Alertes", + "type": "n8n-nodes-base.if", + "typeVersion": 1, + "position": [ + 2180, + 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": [ + 2400, + 300 + ], + "credentials": { + "telegramApi": { + "id": "OyOHh6jAzCDsiBux", + "name": "ND_Telegram" + } + } } -} + ], + "connections": { + "Schedule Trigger (Lundi 08h00)": { + "main": [ + [ + { + "node": "Lire Table Baserow", + "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 + } + ] + ] + }, + "Lire Table Baserow": { + "main": [ + [ + { + "node": "SSH Versions NAS (Restreint)", + "type": "main", + "index": 0 + } + ] + ] + }, + "SSH Versions NAS (Restreint)": { + "main": [ + [ + { + "node": "SSH Versions VPS (Restreint)", + "type": "main", + "index": 0 + } + ] + ] + }, + "SSH Versions VPS (Restreint)": { + "main": [ + [ + { + "node": "Préparer Éléments & Mapping", + "type": "main", + "index": 0 + } + ] + ] + }, + "Préparer Éléments & Mapping": { + "main": [ + [ + { + "node": "API Registres (Docker Hub / npm)", + "type": "main", + "index": 0 + } + ] + ] + }, + "API Registres (Docker Hub / npm)": { + "main": [ + [ + { + "node": "Analyser Écarts & Versions", + "type": "main", + "index": 0 + } + ] + ] + }, + "Analyser Écarts & Versions": { + "main": [ + [ + { + "node": "Mettre à Jour Baserow", + "type": "main", + "index": 0 + } + ] + ] + }, + "Mettre à Jour Baserow": { + "main": [ + [ + { + "node": "Agréger Alertes", + "type": "main", + "index": 0 + } + ] + ] + }, + "Agréger Alertes": { + "main": [ + [ + { + "node": "Filtrer Alertes", + "type": "main", + "index": 0 + } + ] + ] + }, + "Filtrer Alertes": { + "main": [ + [], + [ + { + "node": "Telegram Notification", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": true, + "settings": { + "executionOrder": "v1", + "timezone": "Africa/Tunis" + }, + "versionId": "4e4291da-fa65-4d62-bccc-c83dcf0b3944", + "id": "zUWN5K4Q1A2JKFIS" +} \ No newline at end of file