feat(veille): extension du suivi versions au VPS Contabo et mise a jour DSH/FileBrowser

This commit is contained in:
Nabil Derouiche
2026-08-22 17:06:58 +01:00
parent b1c26fc504
commit 2152b06ac9
3 changed files with 558 additions and 851 deletions
+85
View File
@@ -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@<version> @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`
+56 -34
View File
@@ -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. > **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 ```mermaid
graph TD graph TD
Trigger["Schedule Trigger (Lundi 08h00 Africa/Tunis)"] --> ReadBR["Lire Table Baserow (Table 1097 / Infra Stack)"] Trigger["Schedule Trigger (Lundi 08h00 Africa/Tunis)"] --> ReadBR["Lire Table Baserow (Table 1097 / Infra Stack)"]
ReadBR --> SSHCheck["SSH Restreint NAS (n8n-versions-ro)"] ReadBR --> SSHNAS["SSH Restreint NAS (n8n-versions-ro @ 192.168.100.33:22222)"]
SSHCheck --> MapItems["Préparer Mapping & Éléments"] SSHNAS --> SSHVPS["SSH Restreint VPS (n8n-versions-ro @ tailscale-nyora-bridge:22223)"]
MapItems --> DHub["API Docker Hub (Tags stables)"] SSHVPS --> MapItems["Préparer Mapping & Éléments (Fusion NAS & VPS)"]
DHub --> Analyze["Analyser Écarts & Versions"] MapItems --> Registries["API Registres (Docker Hub & npm registry)"]
Registries --> Analyze["Analyser Écarts & Versions (Boucle 11 items)"]
Analyze --> UpdateBR["Mettre à jour Baserow (PATCH Table 1097)"] Analyze --> UpdateBR["Mettre à jour Baserow (PATCH Table 1097)"]
UpdateBR --> Filter["Filtrer Alertes (Si nouvel écart détecté)"] UpdateBR --> Filter["Filtrer Alertes (Si nouvel écart détecté)"]
Filter -->|Nouvel écart| Telegram["Notification Telegram (ND_Telegram)"] Filter -->|Nouvel écart| Telegram["Notification Telegram (ND_Telegram)"]
@@ -26,16 +27,17 @@ graph TD
## 2. Composants Techniques ## 2. Composants Techniques
### A. Référentiel Baserow (`infra_versions`) ### 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`) - **Database** : `Infra Stack` (ID `317`)
- **Table** : `infra_versions` (ID `1097`) - **Table** : `infra_versions` (ID `1097`)
- **URL** : `https://baserow.bolbol.tn/database/317/table/1097` - **URL** : `https://baserow.bolbol.tn/database/317/table/1097`
- **Token API dédié** : `n8n-infra-versions` (Token ID `32`, scopé Workspace 187 Perso) - **Token API dédié** : `n8n-infra-versions` (Token ID `32`, scopé Workspace 187 Perso)
- **Champs suivis** : - **Champs suivis** :
- `conteneur` (Texte, clé primaire) : Nom du conteneur - `conteneur` (Texte, clé primaire) : Nom du conteneur
- `image_repo` (Texte) : Repository Docker Hub (ex: `n8nio/n8n`, `maximhq/bifrost`) - `image_repo` (Texte) : Repository Docker Hub ou nom de package npm (ex: `@deepseek-ai/dsh`)
- `methode_verif` (Single select) : `image_tag` ou `exec_version` - `methode_verif` (Single select) : `image_tag`, `exec_version` ou `npm_registry`
- `version_installee` (Texte) : Version réelle active sur le NAS - `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 - `derniere_version_disponible` (Texte) : Dernière version stable publiée
- `ecart` (Texte) : Statut de l'écart (`0`, `Installée: ... ➔ Dispo: ...`) - `ecart` (Texte) : Statut de l'écart (`0`, `Installée: ... ➔ Dispo: ...`)
- `criticite` (Single select) : `Critique`, `Important`, `Mineur` - `criticite` (Single select) : `Critique`, `Important`, `Mineur`
@@ -48,20 +50,34 @@ graph TD
--- ---
### B. Accès SSH Restreint NAS (`n8n-versions-ro`) ### 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`). - **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`). - **Clé SSH** : Clé Ed25519 dédiée (`/volume1/docker/scripts/id_ed25519_n8n_versions`).
- **Restriction `authorized_keys`** : - **Restriction `authorized_keys`** :
```text ```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` - **Script exécutable** : `/volume1/docker/scripts/versions-check.sh` (mode 755)
- 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`) ### 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` - **ID Workflow** : `zUWN5K4Q1A2JKFIS`
- **Déclenchement** : Hebdomadaire (Lundi à 08h00 Africa/Tunis) + Webhook de test manuel `/webhook/test-veille-versions`. - **Déclenchement** : Hebdomadaire (Lundi à 08h00 Africa/Tunis) + Webhook de test manuel `/webhook/test-veille-versions`.
- **Canal Telegram** : Credential `ND_Telegram` (`OyOHh6jAzCDsiBux`), Chat ID `2084513684`. - **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 | | Conteneur | Emplacement | Image Repo / Package | Méthode | Criticité | Particularités |
|---|---|---|---|---| |---|---|---|---|---|---|
| `n8n` | `n8nio/n8n` | `exec_version` | Important | Image custom (docx, exceljs) | | `n8n` | NAS | `n8nio/n8n` | `exec_version` | Important | Image custom (docx, exceljs) |
| `bifrost` | `maximhq/bifrost` | `image_tag` | Critique | Pin image, backup config.db/config.json | | `bifrost` | NAS | `maximhq/bifrost` | `image_tag` | Critique | Pin image, backup config.db/config.json |
| `gitea` | `gitea/gitea` | `image_tag` | Important | Pin rootless, backup natif `gitea dump` | | `gitea` | NAS | `gitea/gitea` | `image_tag` | Important | Pin rootless, backup natif `gitea dump` |
| `portainer` | `portainer/portainer-ce` | `image_tag` | Mineur | Pilote stacks, MAJ en priorité | | `portainer` | NAS | `portainer/portainer-ce` | `image_tag` | Mineur | Pilote stacks, MAJ en priorité |
| `vaultwarden` | `vaultwarden/server` | `image_tag` | Mineur | Pin alpine, backup db.sqlite3 | | `vaultwarden` | NAS | `vaultwarden/server` | `image_tag` | Mineur | Pin alpine, backup db.sqlite3 |
| `baserow` | `baserow/baserow` | `exec_version` | Important | Mode minimal, DNS dynamique Nginx | | `baserow` | NAS | `baserow/baserow` | `exec_version` | Important | Mode minimal, DNS dynamique Nginx |
| `crowdsec` | `crowdsecurity/crowdsec` | `exec_version` | Important | Sécurité IDS/IPS DSM | | `crowdsec` | NAS | `crowdsecurity/crowdsec` | `exec_version` | Important | Sécurité IDS/IPS DSM |
| `tailscale-nyora-bridge` | `tailscale/tailscale` | `image_tag` | Mineur | Bridge HTTP/SOCKS5 | | `tailscale-nyora-bridge` | NAS | `tailscale/tailscale` | `image_tag` | Mineur | Bridge HTTP/SOCKS5 |
| `trilium` | `triliumnext/trilium` | `image_tag` | Mineur | `En pause` (exclu des alertes actives) | | `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 ## 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 ```bash
curl -s http://192.168.100.33:5678/webhook/test-veille-versions curl -s http://192.168.100.33:5678/webhook/test-veille-versions -H "Host: n8n.bolbol.tn"
# Réponse attendue : {"no_alert": true, "message": "Aucun nouvel ecart"} # 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 ```bash
ssh -p 22222 -i /volume1/docker/scripts/id_ed25519_n8n_versions -o BatchMode=yes n8n-versions-ro@127.0.0.1 "n8n" ssh -p 22222 -i /volume1/docker/scripts/id_ed25519_n8n_versions -o BatchMode=yes n8n-versions-ro@127.0.0.1 "all"
# Réponse attendue : version exacte (ex: 2.36.5) ```
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"
``` ```
+204 -604
View File
@@ -1,16 +1,5 @@
{ {
"id": "zUWN5K4Q1A2JKFIS",
"name": "veille-versions-stack", "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": [ "nodes": [
{ {
"parameters": { "parameters": {
@@ -18,8 +7,10 @@
"interval": [ "interval": [
{ {
"field": "weeks", "field": "weeks",
"triggerAtHour": 8, "triggerAtDay": [
"triggerAtMinute": 0 1
],
"triggerAtHour": 8
} }
] ]
} }
@@ -33,6 +24,33 @@
300 300
] ]
}, },
{
"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": { "parameters": {
"url": "http://baserow-oauth-proxy:80/api/database/rows/table/1097/?user_field_names=true&size=100", "url": "http://baserow-oauth-proxy:80/api/database/rows/table/1097/?user_field_names=true&size=100",
@@ -82,26 +100,32 @@
}, },
{ {
"parameters": { "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" "command": "all",
"authentication": "privateKey"
}, },
"id": "4", "id": "ssh-vps-node",
"name": "Pr\u00e9parer \u00c9l\u00e9ments & Mapping", "name": "SSH Versions VPS (Restreint)",
"type": "n8n-nodes-base.code", "type": "n8n-nodes-base.ssh",
"typeVersion": 2, "typeVersion": 1,
"position": [ "position": [
860, 860,
300 300
] ],
"credentials": {
"sshPrivateKey": {
"id": "VpsVersionsSsh01",
"name": "SSH n8n-versions-ro-vps"
}
}
}, },
{ {
"parameters": { "parameters": {
"url": "=https://hub.docker.com/v2/repositories/{{ $json.image_repo }}/tags?page_size=25&ordering=last_updated", "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"
"options": {}
}, },
"id": "5", "id": "4",
"name": "API Docker Hub", "name": "Préparer Éléments & Mapping",
"type": "n8n-nodes-base.httpRequest", "type": "n8n-nodes-base.code",
"typeVersion": 4.2, "typeVersion": 2,
"position": [ "position": [
1080, 1080,
300 300
@@ -109,14 +133,41 @@
}, },
{ {
"parameters": { "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" "url": "={{ $json.api_url }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"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", "id": "6",
"name": "Analyser \u00c9carts & Versions", "name": "Analyser Écarts & Versions",
"type": "n8n-nodes-base.code", "type": "n8n-nodes-base.code",
"typeVersion": 2, "typeVersion": 2,
"position": [ "position": [
1300, 1520,
300 300
] ]
}, },
@@ -134,6 +185,10 @@
{ {
"name": "Authorization", "name": "Authorization",
"value": "Token qS5KEYki5sA1VEdCn9hobXk0Bp4jd9fr" "value": "Token qS5KEYki5sA1VEdCn9hobXk0Bp4jd9fr"
},
{
"name": "Content-Type",
"value": "application/json"
} }
] ]
}, },
@@ -164,23 +219,11 @@
}, },
"options": {} "options": {}
}, },
"onError": "continueRegularOutput",
"id": "7", "id": "7",
"name": "Mettre \u00e0 Jour Baserow", "name": "Mettre à Jour Baserow",
"type": "n8n-nodes-base.httpRequest", "type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2, "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": [ "position": [
1740, 1740,
300 300
@@ -188,55 +231,17 @@
}, },
{ {
"parameters": { "parameters": {
"chatId": "2084513684", "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"
"text": "={{ $json.message }}",
"additionalFields": {
"parse_mode": "Markdown"
}
}, },
"id": "9", "id": "8",
"name": "Telegram Notification", "name": "Agréger Alertes",
"type": "n8n-nodes-base.telegram", "type": "n8n-nodes-base.code",
"typeVersion": 1.2, "typeVersion": 2,
"position": [ "position": [
1960, 1960,
300 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": { "parameters": {
"conditions": { "conditions": {
@@ -253,9 +258,32 @@
"type": "n8n-nodes-base.if", "type": "n8n-nodes-base.if",
"typeVersion": 1, "typeVersion": 1,
"position": [ "position": [
1850, 2180,
300 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": { "connections": {
@@ -270,83 +298,6 @@
] ]
] ]
}, },
"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": { "When clicking Test workflow": {
"main": [ "main": [
[ [
@@ -369,6 +320,94 @@
] ]
] ]
}, },
"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": { "Filtrer Alertes": {
"main": [ "main": [
[], [],
@@ -382,450 +421,11 @@
] ]
} }
}, },
"nodeGroups": [], "active": true,
"settings": { "settings": {
"executionOrder": "v1", "executionOrder": "v1",
"timezone": "Africa/Tunis" "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 <contact@bolbol.tn>",
"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", "versionId": "4e4291da-fa65-4d62-bccc-c83dcf0b3944",
"workflowId": "zUWN5K4Q1A2JKFIS", "id": "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-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
}
]
]
}
},
"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": [
{
"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"
}
]
}
} }