cluster-skill — outillage opérateur + docs MacLustr : dépôt canonique sur M1M32 (remote-first, 2026-10-02)
60 changed files +4,448 −0
added
.gitignore
+8 −0
@@ -0,0 +1,8 @@ | ||
| 1 | +cluster-registry.json | |
| 2 | +*.bak | |
| 3 | +*.bak-* | |
| 4 | +.DS_Store | |
| 5 | +maclustr-tunnel/migrate.log | |
| 6 | +maclustr-tunnel/ngrok-retire.log | |
| 7 | +dossier/body.pdf | |
| 8 | +dossier/cover.pdf | |
added
GATEWAY.md
+61 −0
@@ -0,0 +1,61 @@ | ||
| 1 | +# M3U96a — Porte d'entrée (gateway) du cluster MacLustr | |
| 2 | + | |
| 3 | +**Principe.** Pour un projet lourd destiné au cluster, on ne le pousse **qu'une seule fois** | |
| 4 | +depuis le laptop vers **M3U96a** (le nœud le plus puissant : 32 cœurs / 96 Go, gardé vide | |
| 5 | +pour ce rôle depuis le nettoyage du 2026-07-31). M3U96a le **diffuse ensuite aux autres | |
| 6 | +nœuds en parallèle via le réseau local** (192.168.2.x) — pas par internet. Le projet se | |
| 7 | +retrouve au **même chemin sur tous les nœuds** : `~/cluster-projects/<nom>/`. | |
| 8 | + | |
| 9 | +``` | |
| 10 | +laptop ──(Tailscale, 1 seul envoi)──▶ M3U96a:~/cluster-projects/<nom>/ | |
| 11 | + │ | |
| 12 | + └──(LAN 192.168.2.x, 18 rsync parallèles)──▶ tous les nœuds | |
| 13 | +``` | |
| 14 | + | |
| 15 | +## Outil : `cluster-gateway.sh` | |
| 16 | + | |
| 17 | +`~/Desktop/cluster-skill/cluster-gateway.sh` (testé 18/18 nœuds le 2026-07-31, checksums vérifiés). | |
| 18 | + | |
| 19 | +| Commande | Effet | | |
| 20 | +|---|---| | |
| 21 | +| `cluster-gateway.sh push <dir> [nom]` | **le raccourci habituel** : stage + fanout vers tous les nœuds | | |
| 22 | +| `cluster-gateway.sh stage <dir> [nom]` | laptop → M3U96a uniquement | | |
| 23 | +| `cluster-gateway.sh fanout <nom> [nœud …]` | M3U96a → nœuds (défaut : tous sauf M3U96a et M2U64) | | |
| 24 | +| `cluster-gateway.sh list` | projets stagés sur M3U96a | | |
| 25 | +| `cluster-gateway.sh where <nom>` | présence + taille du projet sur chacun des 20 nœuds | | |
| 26 | +| `cluster-gateway.sh clean <nom> [--everywhere]` | efface de M3U96a (ou de partout) | | |
| 27 | + | |
| 28 | +- `[nom]` par défaut = basename du dossier. Re-exécuter `push`/`fanout` fait une mise à jour incrémentale (`rsync --delete` : les nœuds deviennent le miroir exact du staging). | |
| 29 | +- **M2U64 (prod) est exclu par défaut** ; l'inclure avec `--include-m2u64`. | |
| 30 | +- Un fanout partiel se rattrape nœud par nœud : `cluster-gateway.sh fanout <nom> M3BA24`. | |
| 31 | + | |
| 32 | +## Détails techniques (pourquoi c'est fait comme ça) | |
| 33 | + | |
| 34 | +1. **SSH inter-nœuds via Tailscale est bloqué** (timeout port 22, vraisemblablement ACL | |
| 35 | + tailnet) alors que laptop → nœud passe. Le fanout utilise donc les **IP LAN 192.168.2.x**, | |
| 36 | + où SSH nœud-à-nœud fonctionne (auth par agent forwarding, jamais de clé privée sur les nœuds). | |
| 37 | +2. **Les IP LAN sont en DHCP** → le script les résout **en live** à chaque fanout | |
| 38 | + (le laptop demande à chaque nœud son IP du sous-réseau `192.168.2.` sur *toutes* les | |
| 39 | + interfaces — certains MacBooks sont branchés par dongle Ethernet, pas en0/en1). | |
| 40 | + Conséquence : le host-key checking est désactivé pour ces rsync LAN (IP volatiles, | |
| 41 | + réseau privé). | |
| 42 | +3. Le fanout tourne **sur M3U96a** (ssh -A + heredoc), 18 rsync en parallèle ; ~20 Mo | |
| 43 | + vers 18 nœuds ≈ 8 s au premier passage, ~1 s en incrémental. | |
| 44 | +4. Si un nœud est éteint/hors LAN, il est marqué `FAIL` avec la raison — le reste continue. | |
| 45 | + | |
| 46 | +## Convention d'usage pour les jobs distribués | |
| 47 | + | |
| 48 | +- Chemin standard sur tous les nœuds : `~/cluster-projects/<nom>/` — les scripts de | |
| 49 | + calcul distribué peuvent s'y fier (`cd ~/cluster-projects/<nom> && …`). | |
| 50 | +- Workflow type : `push` → lancer le job (`/cluster-run` ou `/cluster-compute`) → | |
| 51 | + récupérer les résultats → `clean <nom> --everywhere` quand le projet est fini. | |
| 52 | +- Gros artefacts communs en lecture (datasets, modèles) : alternative possible via les | |
| 53 | + NAS montés partout (`~/nas_clustr`, `~/nas2_clustr`), mais le fanout LAN reste plus | |
| 54 | + rapide pour l'exécution (disque local). | |
| 55 | + | |
| 56 | +## Garder M3U96a « propre » | |
| 57 | + | |
| 58 | +M3U96a est réservé à ce rôle de passerelle/staging (+ éventuels jobs de calcul) : | |
| 59 | +**ne pas y déployer d'apps/servies persistants** — les apps web vivent sur M2U64 (prod) | |
| 60 | +ou un nœud choisi. Sur M3U96a ne doivent vivre que `~/cluster-projects/` et les | |
| 61 | +services de base (nasmount, tailscale, caffeinate). | |
added
INVENTAIRE-MACLUSTR.md
+177 −0
@@ -0,0 +1,177 @@ | ||
| 1 | +--- | |
| 2 | +title: "Inventaire complet MacLustr" | |
| 3 | +subtitle: "Nœuds internes (LAN Saint-Augustin) et serveurs dédiés OVHcloud — spécifications, rôles, accès et applications" | |
| 4 | +author: "Simon-Pierre Boucher — généré le 10 septembre 2026 à 22 h 40 (état live : mld scan, mld status, mld tunnel status, system_profiler, SSH) ; mis à jour le 2 octobre 2026 (retrait de 4 serveurs OVH)" | |
| 5 | +lang: fr | |
| 6 | +--- | |
| 7 | + | |
| 8 | +## Vue d'ensemble | |
| 9 | + | |
| 10 | +| Catégorie | Machines | Cœurs CPU | RAM | Stockage (brut) | Où | | |
| 11 | +|---|---:|---:|---:|---:|---| | |
| 12 | +| **1. Nœuds internes — LAN MacLustr** | 29 Macs | 393 (nominal 401) | 1 068 Go | ≈ 16,8 To | Saint-Augustin-de-Desmaures, Québec | | |
| 13 | +| **2. Macs loués hors site** | 0 — les 12 Macs loués ont été retirés le 25 septembre 2026 | — | — | — | — | | |
| 14 | +| **3. Serveurs dédiés OVHcloud (Linux)** | 4 serveurs | 38 c / 76 threads x86 | 384 Go | 4 × 2 × 960 Go NVMe (RAID 1) | Beauharnois (Québec) ×3, Gravelines (France) ×1 | | |
| 15 | +| **Total** | **30 machines** | **393 cœurs Apple + 8 cœurs AMD** | **1 132 Go** | ≈ 18,7 To | 2 sites, 1 pays | | |
| 16 | + | |
| 17 | +Les compteurs « nominaux » du cluster (`NODES.md`, `CLAUDE.md`, site www.maclustr.io) comptent 32 cœurs pour chaque M3 Ultra ; `system_profiler` et `mld scan` en voient 28 (20 performance + 8 efficience). Ce document donne les valeurs mesurées. | |
| 18 | + | |
| 19 | +**Mise à jour du 2 octobre 2026.** Le parc OVH est ramené à **un seul serveur, BHS64**, la passerelle publique MacLustr Tunnel de Beauharnois. R9128 (Gravelines, passerelle de secours), BHS64b, BHS128 et BHS128b (Beauharnois) ont été retirés : leurs applications (WebSensor, Market Atlas, hfmarketdata, DataCenterIndex, cancerindex, company-atlas, satelliteindex, InternetPressure) ont été arrêtées et effacées après vérification que tout le code était poussé sur spbgit ; des copies à froid des bases ont été conservées sur M3U96b. Routes publiques, pairs WireGuard, interface `wg0`, alias SSH, inventaires `mld` et `agentd` (3.1.2) mis à jour ; la résiliation des quatre serveurs se fait dans le manager OVH. | |
| 20 | + | |
| 21 | +**Mise à jour du 28 septembre 2026.** Mac mini m6mb (M6 12 cœurs / 24 Go, 192.168.2.106, Tailscale 100.102.242.94) et Mac mini m5ma (M5 Pro 15 cœurs / 24 Go, 192.168.2.112, Tailscale 100.117.72.79) ajoutés au LAN. | |
| 22 | + | |
| 23 | +**Mise à jour du 26 septembre 2026.** Deux Macs ajoutés au LAN : M5M36 (Mac Studio M5 Max 18 cœurs / 36 Go, 192.168.2.104, Tailscale 100.69.26.91) et m6ma (Mac mini M6 12 cœurs / 24 Go, 192.168.2.105, Tailscale 100.83.195.67), macOS 27.0. | |
| 24 | + | |
| 25 | +**Mise à jour du 25 septembre 2026.** Les **12 Macs loués hors site** (Macly, MacStadium, rentamac) ont été retirés du cluster : services arrêtés, tunnel démonté, clés effacées, retirés de tous les inventaires (voir §2). Un Mac Studio M4 Max (M4M64c : 16 cœurs, 64 Go, 494 Go, macOS 26.4, LAN 192.168.2.103, Tailscale 100.125.225.117) ajouté au LAN ; raccordé au MacLustr Tunnel (wg1 10.67.0.33) et durci par `mld prepare`. | |
| 26 | + | |
| 27 | +**Mise à jour du 21 septembre 2026.** Trois Mac mini locaux ajoutés (m4ml, m4mm, m4mn : 34 cœurs, 112 Go). Le serveur DHCP du LAN a renuméroté toutes les adresses 192.168.2.x le même jour ; les colonnes « IP LAN » reflètent l'état `mld discover` du 21 septembre, les noms `*.maclustr.io` (Tailscale) restent inchangés. | |
| 28 | + | |
| 29 | +**Architecture en trois plans.** (1) La **passerelle interne M1M32** héberge le git personnel spbgit et l'orchestrateur `mld`, qui place chaque application sur le nœud le plus efficient, la supervise (`mld heal` toutes les 5 min) et pousse le registre aux consommateurs. (2) L'agent **maclustr-agentd** (M4M64a:9210) sonde tous les nœuds pour les apps macOS et iOS MacLustr et le site www.maclustr.io. (3) La **passerelle publique BHS64** (OVH Beauharnois) termine le TLS des domaines publics (30 routes au 2 octobre 2026) avec Caddy et les relaie aux nœuds par le tunnel WireGuard « MacLustr Tunnel » (hub 10.67.0.1, 16 pairs Mac). C'est la seule passerelle depuis le 2 octobre 2026 (R9128, le hub de secours de Gravelines, a été retiré). Il n'y a plus de ngrok. | |
| 30 | + | |
| 31 | +**Accès.** Tout passe par `ssh <alias>` depuis le laptop (`~/.ssh/config`) avec clés ed25519 et agent forwarding ; trois clés publiques sont autorisées partout : `maclustr-cluster` (laptop), `maclustr-m1m32` (passerelle mld) et `maclustr-agentd` (agent). Les mots de passe sudo et identifiants DeskIn/VNC ne figurent pas ici : voir `NODES.md`. | |
| 32 | + | |
| 33 | +## 1. Nœuds internes — LAN MacLustr (Saint-Augustin-de-Desmaures, Québec) | |
| 34 | + | |
| 35 | +Réseau `192.168.2.x`, domaine `maclustr.io`, Tailscale présent mais **SSH inter-nœuds via Tailscale bloqué** (ACL) : tout passe par les IP LAN. Utilisateur `simon-pierreboucher` partout, home `/Users/simon-pierreboucher`. Tous durcis le 10 septembre 2026 (`mld harden --all` : ouverture de session automatique, pas de veille, redémarrage après coupure, mises à jour automatiques désactivées, FileVault désactivé sur les deux M3 Ultra). PM2 pour les apps, launchd seulement pour l'agent, les gardiens et la console. | |
| 36 | + | |
| 37 | +### 1a. Matériel et système | |
| 38 | + | |
| 39 | +| Alias | Modèle (identifiant) | Puce (P + E) | Cœurs | RAM | SSD total / libre | N° de série | IP LAN | Tunnel wg1 | macOS | État 10 sept. 22 h 30 | | |
| 40 | +|---|---|---|---:|---:|---|---|---|---|---|---| | |
| 41 | +| M3U96a | Mac Studio (Mac15,14) | M3 Ultra (20 + 8) | 28 | 96 Go | 995 / 487 Go | HM62TG64LP | 192.168.2.80 | 10.67.0.10 | 26.6 | en ligne, 17 processus PM2 | | |
| 42 | +| M3U96b | Mac Studio (Mac15,14) | M3 Ultra (20 + 8) | 28 | 96 Go | 995 / 414 Go | J3661QHQ7M | 192.168.2.85 | 10.67.0.11 | 26.4 | en ligne, 8 processus PM2 | | |
| 43 | +| M2U64 | Mac Studio (Mac14,14) | M2 Ultra (16 + 8) | 24 | 64 Go | 995 / 401 Go | T99R24M36K | 192.168.2.61 | 10.67.0.12 | 26.5 | en ligne, 10 processus PM2 | | |
| 44 | +| M4M64a | Mac Studio (Mac16,9) | M4 Max (12 + 4) | 16 | 64 Go | 995 / 848 Go | YFQW7Y1734 | 192.168.2.70 | 10.67.0.13 | 26.5.1 | en ligne, 9 processus PM2 | | |
| 45 | +| M4M64b | Mac Studio (Mac16,9) | M4 Max (12 + 4) | 16 | 64 Go | 995 / 799 Go | F163L9H422 | 192.168.2.77 | 10.67.0.14 | 26.5.1 | en ligne, 19 processus PM2 | | |
| 46 | +| M4M64c | Mac Studio (Mac16,9) | M4 Max (12 + 4) | 16 | 64 Go | 494 / 446 Go | JX0PHFQHF1 | 192.168.2.103 | 10.67.0.33 | 26.4 | en ligne (ajouté 25 sept.) | | |
| 47 | +| M5M36 | Mac Studio (Mac17,14) | M5 Max (6 + 12) | 18 | 36 Go | 494 / 389 Go | LY5Q6FD2PL | 192.168.2.104 | 10.67.0.34 | 27.0 | en ligne (ajouté 26 sept.) | | |
| 48 | +| m6ma | Mac mini (Mac18,5) | M6 | 12 | 24 Go | 494 / 405 Go | FQ452T6L2W | 192.168.2.105 | 10.67.0.35 | 27.0 | en ligne (ajouté 26 sept.) | | |
| 49 | +| m6mb | Mac mini (Mac18,5) | M6 | 12 | 24 Go | 494 / 388 Go | QR7JC5WMYC | 192.168.2.106 | 10.67.0.36 | 27.0 | en ligne (ajouté 28 sept.) | | |
| 50 | +| m5ma | Mac mini (Mac17,16) | M5 Pro (5 + 10) | 15 | 24 Go | 494 / 389 Go | X6YL6N495W | 192.168.2.112 | 10.67.0.37 | 27.0 | en ligne (ajouté 28 sept.) | | |
| 51 | +| M4BP48 | MacBook Pro (Mac16,5) | M4 Max (12 + 4) | 16 | 48 Go | 995 / 931 Go | DRXR751720 | 192.168.2.59 | — | 26.5.1 | en ligne (98 j) | | |
| 52 | +| M4BP36 | MacBook Pro (Mac16,6) | M4 Max (10 + 4) | 14 | 36 Go | 995 / 945 Go | DJ616D4NPF | 192.168.2.86 | — | 26.5.1 | en ligne (rallumé, 6 j) | | |
| 53 | +| M4M36 | Mac Studio (Mac16,9) | M4 Max (10 + 4) | 14 | 36 Go | 494 / 445 Go | FXGPY9V7NT | 192.168.2.67 | 10.67.0.17 | 26.5.2 | en ligne, gardiens launchd | | |
| 54 | +| M2M32 | Mac Studio (Mac14,13) | M2 Max (8 + 4) | 12 | 32 Go | 494 / 365 Go | X5PF2TW170 | 192.168.2.78 | 10.67.0.18 | 26.5.1 | en ligne | | |
| 55 | +| M2M32b | Mac Studio (Mac14,13) | M2 Max (8 + 4) | 12 | 32 Go | 494 / 296 Go | VHKX7M65XC | 192.168.2.69 | — | 26.5.1 | en ligne | | |
| 56 | +| M2M32c | Mac Studio (Mac14,13) | M2 Max (8 + 4) | 12 | 32 Go | 494 / 458 Go | W2QTCJ7TKG | 192.168.2.76 | — | 26.5.1 | en ligne | | |
| 57 | +| m4mc | Mac mini (Mac16,11) | M4 Pro (8 + 4) | 12 | 24 Go | 494 / 456 Go | J742M2X1VC | 192.168.2.75 | — | 26.5.1 | en ligne | | |
| 58 | +| **M1M32** | Mac Studio (Mac13,1) | M1 Max (8 + 2) | 10 | 32 Go | 494 / 376 Go | LVC2Q4R716 | 192.168.2.72 | 10.67.0.2 | 26.5.1 | en ligne, **passerelle** | | |
| 59 | +| m4ma | Mac mini (Mac16,10) | M4 (4 + 6) | 10 | 24 Go | 494 / 457 Go | N6NVTGFFVV | 192.168.2.83 | — | 26.5.1 | en ligne | | |
| 60 | +| m4mb | Mac mini (Mac16,10) | M4 (4 + 6) | 10 | 16 Go | 245 / 209 Go | GYWPHYG6TX | 192.168.2.74 | — | 26.5.1 | en ligne | | |
| 61 | +| m4md | Mac mini (Mac16,10) | M4 (4 + 6) | 10 | 16 Go | 245 Go | — | 192.168.2.62 | — | 26.6.2 | **hors ligne** (éteint depuis la coupure du 10 sept.) | | |
| 62 | +| m4ml | Mac mini (Mac16,10) | M4 (4 + 6) | 10 | 16 Go | 245 Go | VLXC9XWJLY | 192.168.2.63 | 10.67.0.30 | 26.7 | en ligne (ajouté 21 sept.) | | |
| 63 | +| m4mm | Mac mini (Mac16,11) | M4 Pro (8 + 4) | 12 | 48 Go | 494 Go | DYH4X6RW3R | 192.168.2.92 | — (10.67.0.31 réservé) | 26.5.1 | en ligne (ajouté 21 sept.) | | |
| 64 | +| m4mn | Mac mini (Mac16,11) | M4 Pro (8 + 4) | 12 | 48 Go | 494 Go | J322FLVRW6 | 192.168.2.93 | — (10.67.0.32 réservé) | 26.5.1 | en ligne (ajouté 21 sept.) | | |
| 65 | +| m2m16 | Mac mini (Mac14,12) | M2 Pro (6 + 4) | 10 | 16 Go | 494 / 458 Go | LFWH0GVGC6 | 192.168.2.65 | — | 26.4.1 | en ligne (rallumé) | | |
| 66 | +| M3BA24 | MacBook Air (Mac15,12) | M3 (4 + 4) | 8 | 24 Go | 494 / 453 Go | M4KXY1V9CL | 192.168.2.81 | — | 26.5.1 | en ligne (rallumé, 6 j) | | |
| 67 | +| M3BA16 | MacBook Air (Mac15,12) | M3 (4 + 4) | 8 | 16 Go | 494 / 440 Go | LTQX17FW9W | 192.168.2.64 | — | 26.5.1 | en ligne (99 j) | | |
| 68 | +| m2m8a | Mac mini (Mac14,3) | M2 (4 + 4) | 8 | 8 Go | 494 / 456 Go | WQF2N4L6RT | 192.168.2.66 | — | 26.5.1 | en ligne (rallumé) | | |
| 69 | +| m2m8b | Mac mini (Mac14,3) | M2 (4 + 4) | 8 | 8 Go | 245 / 211 Go | Q339C7TMT7 | 192.168.2.68 | — | 26.5.1 | en ligne | | |
| 70 | + | |
| 71 | +**Sous-total LAN : 29 Macs, 393 cœurs mesurés (401 nominaux), 1 068 Go.** 12 Mac Studio, 13 Mac mini, 2 MacBook Pro, 2 MacBook Air. 16 nœuds ont redémarré ensemble le 10 septembre (coupure de courant) et sont revenus seuls : PM2, tunnel wg1 et routes Caddy intacts. Autres équipements sur place : deux NAS UGREEN (DXP4800 Plus `192.168.2.173`, DH4300 Plus `192.168.2.175`). | |
| 72 | + | |
| 73 | +### 1b. Rôle, runtimes et applications par nœud | |
| 74 | + | |
| 75 | +Les applications sont celles du registre `mld status` (source de vérité `M1M32:~/dispatch/registry.json`) ; « runtimes » vient de `mld scan`. Les nœuds sans app sont des **workers libres**, disponibles pour le calcul distribué (`cluster-gateway.sh`) ou un futur `mld deploy`. | |
| 76 | + | |
| 77 | +| Alias | Rôle | Runtimes détectés | Applications (port) | Processus PM2 / launchd | | |
| 78 | +|---|---|---|---|---| | |
| 79 | +| M3U96a | worker apps principal ; ex-passerelle LLM (llm.maclustr.io démantelée le 9 juin 2026) | brew, docker, git, node, pm2, pnpm, postgresql@17, py3.14, redis, tunnel, uv | admin-ka (3300, épinglé, launchd), fetcha (8220), job-ka (8096), polyllm (8240), qc26 (8200), rareindex (8210), sorti-ka (8120), uqo-imm1033 (8161), vrai-prix (8090) | fetcha-api, fetcha-web, job-ka-sync, job-ka-web, polyllm-web, qc26-engine, qc26-web, rareindex-api/-web/-worker, sorti-ka-sync/-web, uqo-imm1033, vrai-prix, vrai-prix-cost-sync, vrai-prix-immoka-sync, vrai-prix-ingest | | |
| 80 | +| M3U96b | **réservé hfmarketdata** (lac de données FirstRate `~/firstratedata`, 353 Go) | brew, docker, git, node, pm2, pnpm, py3.14, redis, tunnel | hfmarketdata (8090) | hfmarketdata-api, -contracts-backfill, -edgar-backfill, -edgar-incremental, -edgar-reconcile, -frd-refresh, -rowgroups, pm2-logrotate | | |
| 81 | +| M2U64 | worker apps, **sans session graphique** (services en LaunchDaemon) | brew, docker, git, node, pm2, pnpm, postgresql@17, py3.14, tunnel | food-ka (8097), maclustr-appmanager (8787), maclustr-console (3000), maclustr-nodepages (8003), spb-cloud (5001), spboucher-ai (3100), uqo-eval (8150), vquant (5000) | 10 processus PM2 (food-ka, app-manager, console, nodepages, spb-cloud, spboucher-ai, uqo-eval + cron cost-sync, vquant) | | |
| 82 | +| M4M64a | worker apps + **maclustr-agentd :9210** (sondes SSH de tout le cluster, API des apps macOS/iOS) + site vitrine www.maclustr.io | brew, docker, git, node, pm2, postgresql@17, py3.14, tunnel, uv | fabri-ka (8097), ka-stats (8140), lou-ka (8095), maclustr-agentd (9210, launchd), maclustr-www (8280, épinglé), pdb-api (8270), uqo-chat (8190) | fabri-ka-sync/-web, ka-stats, lou-ka-sync/-web, maclustr-www, pdb-api, uqo-chat-api, uqo-chat-sandbox | | |
| 83 | +| M4M64b | worker apps le plus chargé (19 processus) ; Postgres 17 + Redis locaux pour api-ka | brew, docker, git, node, pm2, pnpm, postgresql@17, py3.13, py3.14, redis, tunnel | api-ka (8000, `/opt/api-ka`), auto-ka (8095), cancerindex (8250), groupe-ka (8110), immo-ka (8096), uqo-imm1003 (8160), websensor (8260) | apika-api, apika-scheduler, auto-ka-sync/-web, cancerindex-api/-backup/-web/-worker, groupe-ka (+ kaid-alerts, kaid-mf, rapport-quotidien, status-tick), immo-ka-sync/-web, uqo-imm1003, websensor-api/-engine/-web | | |
| 84 | +| M4M64c | worker libre (ajouté 25 sept. 2026) | brew, git, node, pnpm, pm2, py3.13/3.14, uv (via `mld prepare`) | — | — | | |
| 85 | +| M5M36 | worker libre (ajouté 26 sept. 2026) | brew, git, node, pnpm, pm2, py3.13/3.14, uv (via `mld prepare`) | — | — | | |
| 86 | +| m6ma | worker libre (ajouté 26 sept. 2026) | brew, git, node, pnpm, pm2, py3.13/3.14, uv (via `mld prepare`) | — | — | | |
| 87 | +| m6mb | worker libre (ajouté 28 sept. 2026) | brew, git, node, pnpm, pm2, py3.13/3.14, uv (via `mld prepare`) | — | — | | |
| 88 | +| m5ma | worker libre (ajouté 28 sept. 2026) | brew, git, node, pnpm, pm2, py3.13/3.14, uv (via `mld prepare`) | — | — | | |
| 89 | +| M4BP48 | worker libre (portable) | brew, git, uv | — | — | | |
| 90 | +| M4BP36 | worker libre (portable) | brew, git | — | — | | |
| 91 | +| M4M36 | **gardiens Ka** (agents de surveillance, launchd) | brew, git, node, pm2, py3.13, tunnel | ka2 (8799), ka4 (8899), ka6 (8999) | ka2, ka4, ka6 (LaunchAgents) + runners/courrier/pousseur | | |
| 92 | +| M2M32 | **CountryAtlas** (www.countryatlas.co, nœud dédié ; volumes Docker trouve-ka conservés) | brew, docker, git, node, pm2, pnpm, py3.14, tunnel, uv | countryatlas-web (8290), countryatlas-api (8291), countryatlas-scheduler | — | | |
| 93 | +| M2M32b | **SatelliteIndex** (www.satelliteindex.io, nœud dédié) | brew, docker, git, node, pm2, pnpm, postgresql@17, redis, py3.13/3.14, tunnel, uv | satelliteindex-web (8310), satelliteindex-api (8311), satelliteindex-scheduler | Postgres 17 + Redis locaux | | |
| 94 | +| M2M32c | **AI Atlas** (www.ai-atlas.co, nœud dédié) | brew, git, node, pm2, pnpm, postgresql@17, pgvector, redis, py3.12, tunnel, uv | ai-atlas-web (8320), ai-atlas-api (8321), ai-atlas-scheduler | Postgres 17 + pgvector + Redis locaux | | |
| 95 | +| m4mc | worker libre | git, uv | — | — | | |
| 96 | +| **M1M32** | **passerelle du cluster** : spbgit (dépôts bare `~/srv/git/*.git`, git.spboucher.ai :7420), orchestrateur `mld` (`~/dispatch`), `mld heal` (LaunchDaemon io.maclustr.mld-heal, 5 min), alias `gitsrv` ; aucune autre app | brew, git, node, pm2, postgresql@17, tunnel, uv | spbgit (7420) | spbgit | | |
| 97 | +| m4ma | worker libre | git, uv | — | — | | |
| 98 | +| m4mb | worker libre | git | — | — | | |
| 99 | +| m4md | worker libre (hors ligne) | — | — | — | | |
| 100 | +| m4ml | worker libre (ajouté 21 sept. 2026) | brew, git, node, pnpm, pm2, py3.13/3.14, uv (après `mld prepare`) | — | — | | |
| 101 | +| m4mm | worker libre (ajouté 21 sept. 2026) | brew, git, node, pnpm, pm2, py3.13/3.14, uv | — | — | | |
| 102 | +| m4mn | worker libre (ajouté 21 sept. 2026) | brew, git, node, pnpm, pm2, py3.13/3.14, uv | — | — | | |
| 103 | +| m2m16 | worker libre | git | — | — | | |
| 104 | +| M3BA24 | worker libre (portable) | brew, git | — | — | | |
| 105 | +| M3BA16 | worker libre (portable) | brew, git | — | — | | |
| 106 | +| m2m8a | worker libre (8 Go) | brew, git | — | — | | |
| 107 | +| m2m8b | worker libre (8 Go) | git | — | — | | |
| 108 | + | |
| 109 | +## 2. Macs loués hors site — retirés le 25 septembre 2026 | |
| 110 | + | |
| 111 | +Douze Macs loués chez trois fournisseurs (Macly en Turquie : M1M64, m4me, m4mf, m4mh, m1m16b, m4mi, m4mj, m4mk ; MacStadium à Atlanta et Dublin : m2m16b, m1m16, m2m16c ; rentamac à Limassol : m4mg) avaient rejoint le cluster du 8 au 10 septembre 2026 comme nœuds réservés, sans placement automatique. Ils n'ont hébergé qu'une seule application (LLM API sur M1M64). **Le 25 septembre 2026, les douze ont été retirés** : application retirée et routes publiques supprimées, PM2 et tunnel WireGuard démontés, marqueurs et clés MacLustr effacés sur les machines (m4mg n'était plus joignable), pairs et adresses 10.67.0.40–51 libérés sur la passerelle, entrées supprimées de `mld`, de l'agent et des applications macOS/iOS. Le cluster ne compte plus aucun Mac hors du LAN. | |
| 112 | + | |
| 113 | +## 3. Serveurs dédiés OVHcloud (Linux, hors mld / agentd) | |
| 114 | + | |
| 115 | +Un seul serveur depuis le 2 octobre 2026 (BHS64) ; le tableau conserve, barrés, les serveurs retirés ce jour-là. Serveurs bare-metal Ubuntu Server **24.04.4 LTS** (noyau 6.8), utilisateur `ubuntu` avec **sudo sans mot de passe**, root joignable par clé, fuseau America/Toronto, marqueur `/root/maclustr-postinstall.done`. Installés depuis le manager https://manager.ca.ovhcloud.com piloté en Safari/JXA (recette et scripts dans `cluster-skill/ovh-manager/`, ≈ 20 min par serveur). Ce ne sont **pas** des nœuds `mld`/agentd (pas de Homebrew ni de launchd) : on les pilote en SSH direct. Disques : 2 × 960 Go NVMe en RAID 1 logiciel partout (md2 `/boot`, md3 `/` 878 Go). | |
| 116 | + | |
| 117 | +### 3a. Tableau | |
| 118 | + | |
| 119 | +| Alias | Nom OVH (gamme, ID) | CPU | RAM | Réseau | IPv4 / IPv6 | Lieu | Rôle | Services actifs | État 10 sept. 22 h 35 | | |
| 120 | +|---|---|---|---:|---|---|---|---|---|---| | |
| 121 | +| **BHS64** (`ovh2`) | `ns5044661.ip-51-161-112.net` (ADVANCE-2, ID 540625) | AMD EPYC 4345P, 8 c / 16 t, 3,8–5,5 GHz | 64 Go DDR5 ECC 3600 | 3 Gbit/s public, 25 Gbit/s privé | **51.161.112.61** (gw 100.64.0.1) · `2607:5300:22a:3d00::/56` | Beauharnois BHS8, baie BHS0805A03C, `ca-east-bhs-a` | **passerelle principale MacLustr Tunnel** | WireGuard hub `wg0` 10.67.0.1 (udp 51820, 13 pairs), Caddy (36 routes, TLS auto), ufw actif, `tunnelctl` | en ligne (22 h) | | |
| 122 | +| ~~**BHS64b**~~ (`ovh3`) — **retiré le 2 oct. 2026** | `ns5015175.ip-51-161-112.net` (ADVANCE-2) | AMD EPYC 4345P, 8 c / 16 t | 64 Go DDR5 ECC | 3 Gbit/s | 51.161.112.66 | Beauharnois | ex-worker de crawl DataCenterIndex, ex-InternetPressure (12–14 sept.) | aucun (vidé) | à résilier | | |
| 123 | +| ~~**BHS128**~~ (`ovh4`) — **retiré le 2 oct. 2026** | `ns5044426.ip-51-161-112.net` (ADVANCE-1, ID 528691) | AMD EPYC 4245P, 6 c / 12 t | 128 Go DDR5 | 3 Gbit/s | 51.161.112.69 | Beauharnois | ex-DataCenterIndex data, WebSensor, Market Atlas, hfmarketdata (13 sept. → 2 oct.) | aucun (vidé) | à résilier | | |
| 124 | +| ~~**BHS128b**~~ (`ovh5`) — **retiré le 2 oct. 2026** | `ns5035327.ip-51-161-112.net` (ADVANCE-2, ID 503587) | AMD EPYC 4344P, 8 c / 16 t | 128 Go DDR5 | 1 Gbit/s sortant | 51.161.112.85 | Beauharnois | ex-cancerindex, company-atlas, satelliteindex, rareindex, InternetPressure (14 sept. → 2 oct.) | aucun (vidé) | à résilier | | |
| 125 | +| ~~**R9128**~~ (`ovh1`) — **retiré le 2 oct. 2026** | `ns3049362.ip-51-255-75.eu` (RISE-L, ID 1907852) | AMD Ryzen 9 9950X, 16 c / 32 t | 128 Go DDR5 | 1 Gbit/s | 51.255.75.61 | Gravelines (France) | ex-passerelle secondaire du tunnel (hub 10.66.0.1) | aucun (vidé) | à résilier avant le 8 oct. 2026 | | |
| 126 | + | |
| 127 | +### 3b. Fiches | |
| 128 | + | |
| 129 | +- **BHS64** — installé le 10 septembre 2026 (hostname `bhs64`), puis `gateway-setup.sh BHS64 51.161.112.61 10.67.0`. Clé publique WireGuard du hub `QrpSXW8mIJjGMuB3eiP2LQJGpFgV6lTmBG9v0Cxsiz0=`. Les Macs s'y raccordent par leur interface `wg1` (10.67.0.x). Les certificats Let's Encrypt ont été copiés depuis R9128 pour un HTTPS immédiat. Latence ≈ 10 ms depuis Saint-Augustin. Outils : `tunnelctl` sur place, `mlt` sur le laptop (cible par défaut), `mld tunnel …` via M1M32. Toute route est repointée automatiquement à chaque `mld deploy`/`move`. Renouvellement automatique le 10 octobre 2026, sans engagement ; monitoring OVH avec intervention proactive. | |
| 130 | +- **Serveurs retirés le 2 octobre 2026** — BHS64b (jumeau de BHS64, installé le 10 septembre), BHS128 (ADVANCE-1 128 Go, 10 septembre), BHS128b (ADVANCE-2 128 Go, 14 septembre) et R9128 (RISE-L, 8 septembre, première passerelle du tunnel devenue secours). Ils ont hébergé du 12–14 septembre au 2 octobre les plateformes WebSensor, Market Atlas, hfmarketdata, DataCenterIndex, cancerindex, company-atlas, satelliteindex, rareindex et InternetPressure, toutes arrêtées et effacées ce jour-là (code sur spbgit, bases sauvegardées à froid sur M3U96b). | |
| 131 | + | |
| 132 | +## 4. Topologie publique : sites et tunnel | |
| 133 | + | |
| 134 | +Les domaines publics (30 routes au 2 octobre 2026 ; le tableau ci-dessous est l'état du 10 septembre, les sites des serveurs OVH retirés ne répondent plus) arrivent sur BHS64 (DNS `A → 51.161.112.61`) et sont relayés par WireGuard vers le nœud qui porte l'app. `mld` pose et repointe les routes ; `mld heal` répare pairs et routes manquants. | |
| 135 | + | |
| 136 | +| Nœud (wg1) | Sites publics (→ port local) | | |
| 137 | +|---|---| | |
| 138 | +| M3U96a (10.67.0.10) | www.administration-ka.com (3300), www.fetcha.co (8220), www.job-ka.com (8096), www.polyllm.io (8240), www.qc26.ca (8200) + redirection qc26.ca, www.rareindex.io (8210), www.sorti-ka.com (8120), www.uqo-imm1033.app (8161), www.vrai-prix.com (8090) | | |
| 139 | +| M3U96b (10.67.0.11) | www.hfmarketdata.io (8090) | | |
| 140 | +| M2U64 (10.67.0.12) | app.maclustr.io (8787), cloud.spboucher.ai (5001), console.maclustr.io (3000), node.maclustr.io (8003), www.food-ka.com (8097), www.spboucher.ai (3100), www.uqo-eval.app (8150), www.vquant.ai (5000) | | |
| 141 | +| M4M64a (10.67.0.13) | www.fabri-ka.com (8097), www.ka-stats.com (8140), www.lou-ka.com (8095), www.maclustr.io (8280), www.pdb-api.co (8270), www.uqo-chat.app (8190) | | |
| 142 | +| M4M64b (10.67.0.14) | www.api-ka.com (8000), www.auto-ka.com (8095), www.cancerindex.io (8250), www.groupe-ka.com (8110), www.immo-ka.com (8096), www.uqo-imm1003.app (8160), www.websensor.io (8260) | | |
| 143 | +| M4M36 (10.67.0.17) | www.ka2.bot (8799), www.ka4.bot (8899), www.ka6.bot (8999) | | |
| 144 | +| M1M32 (10.67.0.2) | git.spboucher.ai (7420) | | |
| 145 | +| M2M32 (10.67.0.18), m4mh (.47), m1m16b (.48), m4mi (.49), m4mj (.50), m4mk (.51) | raccordés au tunnel, aucune route (prêts à publier) | | |
| 146 | + | |
| 147 | +Autres services internes : maclustr-agentd (M4M64a:9210, API Bearer, routes `/api/cluster`, `/api/apps`, `/api/tunnel`, `/api/events`) alimente les apps macOS MacLustr 3.0.7 et iOS 4.2.0 ; Postgres 17 local sur M3U96a, M2U64, M4M64a, M4M64b, M2M32b et M1M32 ; Redis sur M3U96a, M3U96b, M4M64b, M2M32b ; Docker sur les Studios et M2M32/M2M32b. | |
| 148 | + | |
| 149 | +## 5. Répartitions | |
| 150 | + | |
| 151 | +### 5a. Par emplacement | |
| 152 | + | |
| 153 | +| Emplacement | Fournisseur | Machines | Cœurs | RAM | Alias | | |
| 154 | +|---|---|---:|---:|---:|---| | |
| 155 | +| Saint-Augustin-de-Desmaures, Québec, Canada | propriété | 29 Macs | 393 | 1 068 Go | M3U96a, M3U96b, M2U64, M4M64a, M4M64b, M4M64c, M5M36, m6ma, m6mb, m5ma, M4BP48, M4BP36, M4M36, M2M32, M2M32b, M2M32c, m4mc, M1M32, m4ma, m4mb, m4md, m4ml, m4mm, m4mn, m2m16, M3BA24, M3BA16, m2m8a, m2m8b | | |
| 156 | +| Beauharnois, Québec, Canada | OVHcloud | 3 serveurs | 22 c / 44 t | 256 Go | BHS64, BHS64b, BHS128 | | |
| 157 | +| Gravelines, France | OVHcloud | 1 serveur | 16 c / 32 t | 128 Go | R9128 | | |
| 158 | +| **Total** | **30 machines** | **393 cœurs Apple + 8 cœurs AMD** | **1 132 Go** | ≈ 18,7 To | 2 sites, 1 pays | | |
| 159 | + | |
| 160 | +### 5b. Par modèle et par puce | |
| 161 | + | |
| 162 | +| Famille | Local | Loués | OVH | Total | | |
| 163 | +|---|---:|---:|---:|---:| | |
| 164 | +| Mac Studio | 12 | — | — | 12 | | |
| 165 | +| Mac mini | 13 | — | — | 13 | | |
| 166 | +| MacBook Pro | 2 | — | — | 2 | | |
| 167 | +| MacBook Air | 2 | — | — | 2 | | |
| 168 | +| Serveur x86 (AMD EPYC 4345P) | — | — | 1 | 1 | | |
| 169 | +| Puces Apple : M6 (base ×2) 2 · M5 (Max ×1, Pro ×1) 2 · M4 (Max ×6, Pro ×3, base ×4) 13 · M3 (Ultra ×2, base ×2) 4 · M2 (Ultra ×1, Max ×3, Pro ×1, base ×2) 7 · M1 (Max ×1) 1 | | | | 29 | | |
| 170 | + | |
| 171 | +### 5c. Capacité libre | |
| 172 | + | |
| 173 | +Au 10 septembre 2026 : 14 nœuds LAN sans app (150 cœurs, 332 Go) et 2 serveurs OVH sans rôle (BHS64b 16 t / 64 Go, BHS128 12 t / 128 Go). Les apps tournent sur 7 nœuds LAN seulement (M3U96a, M3U96b, M2U64, M4M64a, M4M64b, M4M36, M1M32), avec entre 50 et 80 Go de RAM libre sur chaque Studio 64–96 Go. | |
| 174 | + | |
| 175 | +## 6. Sources et méthode | |
| 176 | + | |
| 177 | +`mld scan`, `mld status`, `mld tunnel status` (22 h 30), `system_profiler SPHardwareDataType`, `sw_vers`, `sysctl hw.model`, `df -H`, `uptime`, `pm2 jlist` sur les 33 Macs ; `lscpu`, `free`, `/proc/mdstat`, `systemctl`, `ufw status` sur les serveurs OVH (4 le 10 septembre, 1 depuis le 2 octobre 2026) ; fiches du manager OVH (ID, baie, IPv6) ; `NODES.md`, `~/.ssh/config`, `mld/config.py`, `agentd.py`. Géolocalisation des Macs loués : whois / ip-api et temps de connexion mesurés le 9 septembre. Documents complémentaires : `NODES.md` (identifiants, sudo), `MACLUSTR-TUNNEL.md` (tunnel), `KA-REMOTE-DEV.md` (apps Ka), `ovh-manager/README.md` (installation OVH), `cluster-registry.json` (registre mld). | |
added
INVENTAIRE-MACLUSTR.pdf
+0 −0
Binary file not shown.
added
INVENTAIRE-MACS.md
+118 −0
@@ -0,0 +1,118 @@ | ||
| 1 | +--- | |
| 2 | +title: "Inventaire des Macs du cluster MacLustr" | |
| 3 | +subtitle: "Parc local (Saint-Augustin-de-Desmaures, Québec) et Macs loués hors site" | |
| 4 | +author: "Simon-Pierre Boucher — généré le 9 septembre 2026, mis à jour le 10 septembre 2026" | |
| 5 | +lang: fr | |
| 6 | +--- | |
| 7 | + | |
| 8 | +## Vue d'ensemble | |
| 9 | + | |
| 10 | +| | Macs | Cœurs | RAM | Où | | |
| 11 | +|---|---:|---:|---:|---| | |
| 12 | +| **Parc local (LAN MacLustr)** | 21 | 294 | 784 Go | Saint-Augustin-de-Desmaures, Québec, Canada | | |
| 13 | +| **Loués — Macly** | 8 | 78 | 176 Go | Turquie (Istanbul / Uşak) | | |
| 14 | +| **Loués — MacStadium** | 3 | 26 | 48 Go | Atlanta (États-Unis) et Dublin (Irlande) | | |
| 15 | +| **Loués — rentamac** | 1 | 10 | 16 Go | Limassol (Chypre) | | |
| 16 | +| **Total** | **33** | **408** | **1 024 Go** | 5 pays, 6 sites | | |
| 17 | + | |
| 18 | +Le serveur Linux OVHcloud R9128 (Gravelines, France) n'est pas un Mac et n'apparaît pas ici. Les mots de passe sudo et les identifiants DeskIn/VNC sont dans `NODES.md`, pas dans ce document. | |
| 19 | + | |
| 20 | +## 1. Parc local — Saint-Augustin-de-Desmaures (Québec) | |
| 21 | + | |
| 22 | +Réseau LAN `192.168.2.x`, domaine `maclustr.io`, Tailscale. Utilisateur `simon-pierreboucher` partout, clés ed25519 sans mot de passe. Passerelle du cluster : **M1M32** (spbgit + orchestrateur `mld`). État au 8 septembre 2026. | |
| 23 | + | |
| 24 | +| Alias | Modèle | Puce | Cœurs | RAM | Disque | IP LAN | macOS | Rôle | État | | |
| 25 | +|---|---|---|---:|---:|---:|---|---|---|---| | |
| 26 | +| M3U96a | Mac Studio | M3 Ultra | 32 | 96 Go | 926 Go | 192.168.2.87 | 26.6 | worker apps + passerelle LLM llm.maclustr.io | en ligne | | |
| 27 | +| M3U96b | Mac Studio | M3 Ultra | 32 | 96 Go | 926 Go | 192.168.2.82 | 26.4 | réservé hfmarketdata | en ligne | | |
| 28 | +| M2U64 | Mac Studio | M2 Ultra | 24 | 64 Go | 926 Go | 192.168.2.107 | 26.5 | worker (sans session graphique) | en ligne | | |
| 29 | +| M4M64a | Mac Studio | M4 Max | 16 | 64 Go | 926 Go | 192.168.2.83 | 26.5.1 | worker + maclustr-agentd :9210 | en ligne | | |
| 30 | +| M4M64b | Mac Studio | M4 Max | 16 | 64 Go | 926 Go | 192.168.2.78 | 26.5.1 | worker | en ligne | | |
| 31 | +| M4BP48 | MacBook Pro | M4 Max | 16 | 48 Go | 926 Go | 192.168.2.80 | 26.5.1 | worker | en ligne | | |
| 32 | +| M4BP36 | MacBook Pro | M4 Max | 14 | 36 Go | — | 192.168.2.133 | — | worker | éteint | | |
| 33 | +| M4M36 | Mac Studio | M4 Max | 14 | 36 Go | 460 Go | 192.168.2.69 | 26.5.2 | worker, gardiens Ka | en ligne | | |
| 34 | +| M2M32 | Mac Studio | M2 Max | 12 | 32 Go | 460 Go | 192.168.2.90 | 26.5.1 | worker | en ligne | | |
| 35 | +| M2M32b | Mac Studio | M2 Max | 12 | 32 Go | 460 Go | 192.168.2.77 | 26.5.1 | worker | en ligne | | |
| 36 | +| M2M32c | Mac Studio | M2 Max | 12 | 32 Go | 460 Go | 192.168.2.89 | 26.5.1 | worker | en ligne | | |
| 37 | +| m4mc | Mac mini | M4 Pro | 12 | 24 Go | 460 Go | 192.168.2.75 | 26.5.1 | worker | en ligne | | |
| 38 | +| **M1M32** | Mac Studio | M1 Max | 10 | 32 Go | 460 Go | 192.168.2.76 | 26.5.1 | **passerelle** (spbgit, mld) | en ligne | | |
| 39 | +| m4ma | Mac mini | M4 | 10 | 24 Go | 460 Go | 192.168.2.73 | 26.5.1 | worker | en ligne | | |
| 40 | +| m4mb | Mac mini | M4 | 10 | 16 Go | 228 Go | 192.168.2.74 | 26.5.1 | worker | en ligne | | |
| 41 | +| m4md | Mac mini | M4 | 10 | 16 Go | 228 Go | 192.168.2.110 | 26.6.2 | worker | en ligne | | |
| 42 | +| m2m16 | Mac mini | M2 Pro | 10 | 16 Go | — | 192.168.2.85 | — | worker | éteint | | |
| 43 | +| M3BA24 | MacBook Air | M3 | 8 | 24 Go | — | 192.168.2.81 | — | worker | éteint | | |
| 44 | +| M3BA16 | MacBook Air | M3 | 8 | 16 Go | 460 Go | 192.168.2.70 | 26.5.1 | worker | en ligne | | |
| 45 | +| m2m8a | Mac mini | M2 | 8 | 8 Go | — | 192.168.2.84 | — | worker | éteint | | |
| 46 | +| m2m8b | Mac mini | M2 | 8 | 8 Go | 228 Go | 192.168.2.72 | 26.5.1 | worker | en ligne | | |
| 47 | + | |
| 48 | +**Sous-total : 21 Macs, 294 cœurs, 784 Go** (17 en ligne, 4 éteints). Répartition : 11 Mac Studio, 7 Mac mini, 2 MacBook Pro, 2 MacBook Air (dont 1 Mac Studio passerelle). Autres équipements sur place : 2 NAS UGREEN (DXP4800 Plus `.173`, DH4300 Plus `.175`). | |
| 49 | + | |
| 50 | +## 2. Macs loués hors site | |
| 51 | + | |
| 52 | +Nœuds `REMOTE_NODES` dans mld, **réservés** (déploiement seulement avec `mld deploy <app> --node <alias>`), pas de Tailscale ni de LAN vers les apps sœurs. PM2 en LaunchDaemon système. Localisation établie par le réseau (AS / géolocalisation IP) et confirmée par les temps de connexion mesurés depuis Québec et depuis Gravelines (France). | |
| 53 | + | |
| 54 | +### 2a. Turquie — fournisseur Macly (AS199099 Erlion Bilişim) | |
| 55 | + | |
| 56 | +Accès SSH par clé seulement (utilisateur `simon`, `AllowUsers` du fournisseur complété par `000-maclustr.conf`), prise en main graphique via DeskIn. Latence ≈ 147 ms depuis Québec, ≈ 50 ms depuis la France. | |
| 57 | + | |
| 58 | +| Alias | Modèle (identifiant) | Puce | Cœurs | RAM | Disque | N° de série | IP publique | macOS | Ville (géoloc IP) | Ajouté | | |
| 59 | +|---|---|---|---:|---:|---:|---|---|---|---|---| | |
| 60 | +| M1M64 | Mac Studio (Mac13,1) | M1 Max | 10 | 64 Go | 1,8 To | WWVV29HYQW | 45.74.241.221 | 26.6.2 | Istanbul | 2026-09-08 | | |
| 61 | +| m4me | Mac mini (Mac16,10) | M4 | 10 | 16 Go | 228 Go | D46D7Q4XFL | 45.74.245.36 | 26.6.2 | Uşak | 2026-09-08 | | |
| 62 | +| m4mf | Mac mini (Mac16,10) | M4 | 10 | 16 Go | 228 Go | XY0YVKXV50 | 45.74.245.37 | 26.6.2 | Uşak | 2026-09-09 | | |
| 63 | +| m4mh | Mac mini (Mac16,10) | M4 | 10 | 16 Go | 228 Go | H9FT6DP6V9 | 45.74.241.182 | 26.6.2 | Istanbul | 2026-09-10 | | |
| 64 | +| m1m16b | Mac mini (Macmini9,1 — vendu « M4 ») | M1 | 8 | 16 Go | 228 Go | C07J70PHQ6NY | 45.74.241.171 | 26.6.1 | Istanbul | 2026-09-10 | | |
| 65 | +| m4mi | Mac mini (Mac16,10) | M4 | 10 | 16 Go | 228 Go | VW6R45M7HW | 45.74.245.39 | 26.6.2 | Uşak | 2026-09-10 | | |
| 66 | +| m4mj | Mac mini (Mac16,10) | M4 | 10 | 16 Go | 228 Go | QX4L7DQFJH | 45.74.245.38 | 26.6.2 | Uşak | 2026-09-10 | | |
| 67 | +| m4mk | Mac mini (Mac16,10) | M4 | 10 | 16 Go | 228 Go | DK65R60V95 | 45.74.241.98 | 26.6.2 | Istanbul | 2026-09-10 | | |
| 68 | + | |
| 69 | +### 2b. États-Unis, Atlanta (Géorgie) — MacStadium (AS395336) | |
| 70 | + | |
| 71 | +Utilisateur `administrator`, mot de passe SSH accepté, partage d'écran VNC sur le port 45900. Latence ≈ 62 ms depuis Québec. | |
| 72 | + | |
| 73 | +| Alias | Modèle (identifiant) | Puce | Cœurs | RAM | Disque | N° de série | IP publique | macOS | Ajouté | | |
| 74 | +|---|---|---|---:|---:|---:|---|---|---|---| | |
| 75 | +| m2m16b | Mac mini (Mac14,12) | M2 Pro | 10 | 16 Go | 926 Go | TQV5L6R9MK | 208.52.154.68 | 26.6 | 2026-09-08 | | |
| 76 | + | |
| 77 | +### 2c. Irlande, Dublin — MacStadium (AS30377) | |
| 78 | + | |
| 79 | +Même accès que 2b. Latence ≈ 96 ms depuis Québec, ≈ 17 ms depuis la France. | |
| 80 | + | |
| 81 | +| Alias | Modèle (identifiant) | Puce | Cœurs | RAM | Disque | N° de série | IP publique | macOS | Ajouté | | |
| 82 | +|---|---|---|---:|---:|---:|---|---|---|---| | |
| 83 | +| m1m16 | Mac mini (Macmini9,1) | M1 | 8 | 16 Go | 926 Go | H2WGW18RQ6P0 | 207.254.29.71 | 26.6.2 | 2026-09-08 | | |
| 84 | +| m2m16c | Mac mini (Mac14,3) | M2 | 8 | 16 Go | 926 Go | X0KPDY0DQ5 | 207.254.29.128 | 26.6.2 | 2026-09-08 | | |
| 85 | + | |
| 86 | +### 2d. Chypre, Limassol — rentamac | |
| 87 | + | |
| 88 | +Mac derrière le NAT du fournisseur (réseau CYTA, Limassol) : pas de port 22 direct, SSH par la passerelle du fournisseur `gate1.rentamac.io:27335` (relais Hetzner, Nuremberg). Utilisateur `rentamac`, mot de passe SSH accepté, prise en main graphique via DeskIn. Latence ≈ 111 ms depuis Québec jusqu'au relais. | |
| 89 | + | |
| 90 | +| Alias | Modèle (identifiant) | Puce | Cœurs | RAM | Disque | N° de série | Accès SSH | macOS | Ajouté | | |
| 91 | +|---|---|---|---:|---:|---:|---|---|---|---| | |
| 92 | +| m4mg | Mac mini (Mac16,10) | M4 | 10 | 16 Go | 228 Go | R7023K3WF0 | gate1.rentamac.io:27335 (Mac : 10.10.114.120, sortie 81.4.164.154) | 26.6.2 | 2026-09-09 | | |
| 93 | + | |
| 94 | +**Sous-total loués : 12 Macs, 114 cœurs, 240 Go**, tous en ligne, aucune app déployée au 10 septembre 2026. Runtimes installés par `mld prepare` : Xcode CLT, Homebrew, node 26, pnpm, pm2, ngrok, Python 3.13/3.14, uv. | |
| 95 | + | |
| 96 | +## 3. Répartition par emplacement | |
| 97 | + | |
| 98 | +| Emplacement | Fournisseur | Macs | Cœurs | RAM | Alias | | |
| 99 | +|---|---|---:|---:|---:|---| | |
| 100 | +| Saint-Augustin-de-Desmaures, Québec, Canada | propriété | 21 | 294 | 784 Go | M3U96a, M3U96b, M2U64, M4M64a, M4M64b, M4BP48, M4BP36, M4M36, M2M32, M2M32b, M2M32c, m4mc, M1M32, m4ma, m4mb, m4md, m2m16, M3BA24, M3BA16, m2m8a, m2m8b | | |
| 101 | +| Istanbul, Turquie | Macly | 4 | 38 | 112 Go | M1M64, m4mh, m1m16b, m4mk | | |
| 102 | +| Uşak, Turquie | Macly | 4 | 40 | 64 Go | m4me, m4mf, m4mi, m4mj | | |
| 103 | +| Atlanta, Géorgie, États-Unis | MacStadium | 1 | 10 | 16 Go | m2m16b | | |
| 104 | +| Dublin, Irlande | MacStadium | 2 | 16 | 32 Go | m1m16, m2m16c | | |
| 105 | +| Limassol, Chypre | rentamac | 1 | 10 | 16 Go | m4mg | | |
| 106 | +| **Total** | | **33** | **408** | **1 024 Go** | | | |
| 107 | + | |
| 108 | +## 4. Répartition par modèle | |
| 109 | + | |
| 110 | +| Modèle | Local | Loués | Total | | |
| 111 | +|---|---:|---:|---:| | |
| 112 | +| Mac Studio | 11 | 1 | 12 | | |
| 113 | +| Mac mini | 7 | 11 | 18 | | |
| 114 | +| MacBook Pro | 2 | 0 | 2 | | |
| 115 | +| MacBook Air | 2 | 0 | 2 | | |
| 116 | +| Puces : M4 (Max/Pro/base) 16 · M3 (Ultra/base) 4 · M2 (Ultra/Max/Pro/base) 9 · M1 (Max/base) 4 | | | 33 | | |
| 117 | + | |
| 118 | +Sources : `mld/config.py`, `agentd.py`, `ClusterManager.swift`, `NODES.md`, scans live `mld scan` du 8 et 9 septembre 2026, `system_profiler` sur les Macs loués, géolocalisation ip-api.com + whois, temps de connexion TCP mesurés le 9 septembre 2026. | |
added
INVENTAIRE-MACS.pdf
+0 −0
Binary file not shown.
added
KA-REMOTE-DEV.md
+66 −0
@@ -0,0 +1,66 @@ | ||
| 1 | +# Groupe Ka — Développement direct sur les nœuds (remote-first) | |
| 2 | + | |
| 3 | +> **MAJ 2026-09-04 — passerelle M1M32 + `mld`.** spbgit (bare repos `~/srv/git/*.git`, git.spboucher.ai) a déménagé de M3U96a vers **M1M32**. L'alias `Host gitsrv` de chaque nœud (et du laptop) pointe désormais vers M1M32 (`MacStudio-de-simon-pierre-8.local` en LAN, `M1M32.maclustr.io` depuis le laptop), clé `~/.ssh/ka_guardian_ed25519` par nœud autorisée sur M1M32 ; les repos de M3U96a n'utilisent plus de chemin local. Les apps sont déployées/migrées par l'orchestrateur **maclustr-dispatch** (`~/Desktop/cluster-skill/mld …`, registre `cluster-registry.json`) : **les nœuds ci-dessous peuvent changer — `mld status` fait foi.** Nouveaux emplacements du 2026-09-04 : immo-ka → M3U96b, house-ka → M3U96a, resto-ka → M4M64a, food-ka → M2U64, rent-ka → M4M64b, ka-stats → M4M64a, uqo-eval → M2U64. Pousseur Ka (`com.ka.pousseur`) installé sur M3U96a/b, M2U64, M4M64a/b, M4M36, M2M32 ; `~/.ka-pousseur-repos` mis à jour automatiquement par `mld` (inscription à l'arrivée, retrait au départ). Les anciens `~/srv/git` et `~/srv/spbgit` de M3U96a sont renommés `*.old-20260904` (backups 20 Go inclus). | |
| 4 | + | |
| 5 | +> **MAJ 2026-09-04 (soir) — consommateurs du registre.** Les gardiens **ka2/ka4/ka6** (`orchestrator/registry.py`, copie `M4M36:~/ka-guardian-spool/registry.json` poussée par `mld` + tirée par le launchd `com.ka.registry-sync`) et la console **admin-ka** (`server/registry.js`, copie `~/apps/admin-ka/data/registry.json`, tirage ssh 2 min, `~/ka-orchestrator/CLAUDE.md` régénéré) lisent désormais **le registre mld** pour savoir où tourne chaque app : nœud, IP LAN, répertoire, port, process PM2. Plus aucune table d'emplacements en dur chez eux ; `mld subscribers` sur M1M32 liste les abonnés. Après un `mld move` d'une app Ka vers un nœud jamais équipé : `~/Desktop/Groupe-KA/apps-web/ka-guardian/deploy/deploy.sh runners <nœud>` (+ `courier`, `pousseur`) — la liste des nœuds y est elle aussi dérivée du registre ; admin-ka pose seul `~/.adminka/perm-mcp.js` + `~/.claude/CLAUDE.md` sur le nouveau nœud. ⚠️ **M2U64 n'a pas de session graphique** (console = root) : pas de domaine launchd `gui/501` → les services y sont des **LaunchDaemons** système (`/Library/LaunchDaemons/com.ka.*`, `UserName` + `HOME`), posés par `deploy.sh` via sudo (`KA_SUDO_PW` dans `~/.ka-guardian.env` du laptop). | |
| 6 | + | |
| 7 | + | |
| 8 | +**Depuis le 2026-08-17, la source de vérité des 12 projets Ka est le repo git SUR LE NŒUD de déploiement**, plus les copies `~/Desktop/*` du laptop. Toute session Claude sur le laptop travaille sur le repo du nœud via SSH : édition, build, restart PM2, `git commit` et `git push` se font sur le nœud. | |
| 9 | + | |
| 10 | +## Table des projets | |
| 11 | + | |
| 12 | +| App | Nœud | Dir sur le nœud | Port | Domaine | Process PM2 | | |
| 13 | +|------------|---------|-------------------|-------|----------------------|-------------| | |
| 14 | +| groupe-ka | M3U96b | `~/apps/groupe-ka`| 8110 | www.groupe-ka.com | groupe-ka + groupe-ka-ngrok | | |
| 15 | +| lou-ka | M3U96b | `~/apps/lou-ka` | 8095 | www.lou-ka.com | lou-ka-web + lou-ka-sync + lou-ka-ngrok | | |
| 16 | +| api-ka | M3U96b | `/opt/api-ka` | 8000 | www.api-ka.com | apika-api + apika-scheduler + apika-ngrok | | |
| 17 | +| crea-ka | M3U96b | `~/apps/crea-ka` | 8160 | www.crea-ka.com | crea-ka-web + crea-ka-sync + crea-ka-ngrok | | |
| 18 | +| resto-ka | M3U96b | `~/apps/resto-ka` | 8115 | www.resto-ka.com | resto-ka + resto-ka-sync + resto-ka-ngrok | | |
| 19 | +| vrai-prix | M3U96a | `~/apps/vrai-prix`| 8090 | www.vrai-prix.com | vrai-prix + vrai-prix-ngrok + vrai-prix-ingest (cron 08:00) + vrai-prix-cost-sync (cron 05:15, connecteurs méthode du coût → data/cost.db) + vrai-prix-immoka-sync (cron 03:30, snapshot Immo-Ka depuis M4M64b via LAN + ré-évaluation → data/immoka.db, table vp_eval, data/marche-stats.json) — ajoutés 2026-09-08 ; data/ hors git et hors sync mld ; épinglé M3U96a | | |
| 20 | +| sorti-ka | M3U96a | `~/apps/sorti-ka` | 8120 | www.sorti-ka.com | sorti-ka-web + sorti-ka-sync + sorti-ka-ngrok | | |
| 21 | +| house-ka | M4M64b | `~/apps/house-ka` | 8098 | www.house-ka.com | house-ka-web + house-ka-sync + house-ka-ngrok | | |
| 22 | +| immo-ka | M4M64a | `~/apps/immo-ka` | 8096 | www.immo-ka.com | immo-ka-web + immo-ka-sync + immo-ka-ngrok | | |
| 23 | +| fabri-ka | M4M64a | `~/fabri-ka` | 8097 | www.fabri-ka.com | fabri-ka-web + fabri-ka-sync + fabri-ka-ngrok | | |
| 24 | +| auto-ka | M4M64b | `~/auto-ka` | 8095 | www.auto-ka.com | auto-ka-web + auto-ka-sync + auto-ka-ngrok | | |
| 25 | +| food-ka | M4M64b | `~/apps/food-ka` | 8097 | www.food-ka.com | food-ka-web + food-ka-sync + food-ka-ngrok | | |
| 26 | +| trouve-ka | M2M32 | `~/trouve-ka` | 3000 | www.trouve-ka.com | tk-web/tk-api/tk-crawler-1,2/tk-enrichment/tk-scheduler + docker (postgres, redis) + com.trouveka.ngrok | | |
| 27 | +| home-ka | M4M36 | `~/apps/home-ka` | 8099 | www.home-ka.com | home-ka-web + home-ka-sync + home-ka-ngrok | | |
| 28 | + | |
| 29 | +Détails de déploiement (start commands, notes historiques) : `~/Desktop/cluster-skill/cluster-deployments.json`. | |
| 30 | + | |
| 31 | +## Serveur git central = spbgit (git perso de Simon-Pierre) | |
| 32 | + | |
| 33 | +- Le serveur git est **spbgit**, le git personnel : https://git.spboucher.ai (PAS GitHub — le compte GitHub spboucher-ai a déjà été suspendu, ne compter que sur spbgit). Stockage : bare repos `~/srv/git/<app>.git` sur **M3U96a**. | |
| 34 | +- Remote `origin` de chaque repo de nœud : | |
| 35 | + - Sur M3U96a (vrai-prix, sorti-ka) : chemin local `/Users/simon-pierreboucher/srv/git/<app>.git`. | |
| 36 | + - Sur les autres nœuds : `gitsrv:srv/git/<app>.git` — alias SSH `gitsrv` ajouté en **tête** du `~/.ssh/config` de M3U96b, M4M64a, M4M64b et M2M32 (2026-08-17) : | |
| 37 | + ``` | |
| 38 | + Host gitsrv | |
| 39 | + ControlMaster no | |
| 40 | + ControlPath none | |
| 41 | + HostName MacStudio-de-simon-pierre-2.local # M3U96a via mDNS LAN (192.168.2.87) | |
| 42 | + User simon-pierreboucher | |
| 43 | + StrictHostKeyChecking accept-new | |
| 44 | + ``` | |
| 45 | + ⚠️ SSH inter-nœuds via Tailscale (`*.maclustr.io`) est bloqué par ACL → `gitsrv` passe par le LAN/mDNS. ⚠️ Le bloc doit rester AVANT `Host *` (sinon le ControlPath global casse : « socket path too long »). ⚠️ `ControlMaster no` obligatoire (même raison). | |
| 46 | +- **Push depuis un nœud** : fonctionne grâce à l'agent forwarding (le laptop a `ForwardAgent yes` global) — donc pousser pendant une session SSH ouverte depuis le laptop. **Depuis le 2026-08-24, le push AUTONOME marche aussi** (missions headless des gardiens ka2/ka4/ka6, cron) : la clé `~/.ssh/ka_guardian_ed25519` de chaque nœud (M3U96b, M4M64a, M4M64b) est autorisée sur M3U96a et déclarée en `IdentityFile` dans le bloc `Host gitsrv` — vérifié sans agent SSH. Sur M3U96a l'origin est un chemin local (aucune clé requise). Les consignes des gardiens exigent désormais `git push origin main` après chaque mission. ⚠️ MAIS les missions elles-mêmes (descendants du runner python) sont bloquées par macOS 26 LNP (« no route to host ») : le **pousseur launchd `com.ka.pousseur`** (chaîne binaires Apple, repos dans `~/.ka-pousseur-repos`, tick 5 min) pousse les commits en attente sur M3U96a/b + M4M64a/b. Le rollback du runner est **ciblé** : il ne reverte que les commits `[ka*]` (jamais reset --hard ni revert de plage) puis laisse le pousseur pousser les reverts. | |
| 47 | + | |
| 48 | +## Workflow Claude (laptop) | |
| 49 | + | |
| 50 | +1. **Lire** : `ssh <node> "cd <dir> && git status && git log --oneline -5"`. | |
| 51 | +2. **Éditer** : petits changements via `ssh` (sed/python heredoc) ; changements substantiels : `scp <node>:<dir>/<fichier> /tmp/`, éditer localement avec les outils, `scp` retour. Ne jamais éditer les copies `~/Desktop/*`. | |
| 52 | +3. **Builder/tester sur le nœud** (venv/node_modules y sont déjà), puis `pm2 restart <process>` et healthcheck. | |
| 53 | +4. **Committer + pousser sur le nœud** : `ssh <node> "cd <dir> && git add -A && git commit -m '...' && git push origin main"`. | |
| 54 | +5. **Mettre à jour** `cluster-deployments.json` (notes + `updated`) comme d'habitude. | |
| 55 | + | |
| 56 | +## Copies laptop (`~/Desktop/*`) — statut | |
| 57 | + | |
| 58 | +Miroirs SECONDAIRES, ne plus y travailler (faire `git pull origin main` pour consulter) : groupe-ka, lou-ka, qc_house_eval/vrai-prix, qc-store (=fabri-ka), food-ka, api-ka, trouve-ka, crea-ka, resto-ka, sorti-ka. **immo-ka et auto-ka n'ont AUCUNE copie laptop** — le nœud + bare repo sont les seules copies. | |
| 59 | + | |
| 60 | +## État initial de la migration (2026-08-17) | |
| 61 | + | |
| 62 | +Repos git installés dans les 12 dirs de nœud, HEAD aligné sur `origin/main` sans toucher aux fichiers (apps non interrompues). Fichiers trackés manquants restaurés (`git checkout`) : fabri-ka `data/enrich_cache/` (3278 fichiers, jamais rsyncés), lou-ka docs ×7, immo-ka frontend ×7, api-ka logs ×1, trouve-ka docs ×1. `.claude/` exclu du statut via `.git/info/exclude` partout. | |
| 63 | + | |
| 64 | +WIP laptop non commité transféré/vérifié sur les nœuds : food-ka (SSO KA ID + favoris — backend déjà présent, 7 fichiers frontend rsyncés), resto-ka et groupe-ka (déjà identiques sur nœud). | |
| 65 | + | |
| 66 | +**Drift entièrement committé + poussé le 2026-08-17 — les 12 repos de nœud sont PROPRES** (commits « État du nœud / WIP committé depuis le nœud » : lou-ka c556557, groupe-ka 6c76595, resto-ka c9f9e09, food-ka 6004efe, immo-ka 5a9aa9d, fabri-ka 20a2569, sorti-ka 69b8641). Gitignores ajoutés à cette occasion : lou-ka `data/uploads/`, sorti-ka `*.log`, immo-ka `data/vraiprix.db` (1,6 Go) + `*.bak-*` ; trouve-ka `services/tmp-stage/` exclu via `.git/info/exclude` (doublons exacts de fichiers trackés). Les copies laptop sont maintenant EN RETARD sur origin — faire `git pull` avant toute consultation. | |
added
MACLUSTR-DOSSIER.pdf
+0 −0
Binary file not shown.
added
MACLUSTR-TUNNEL.md
+68 −0
@@ -0,0 +1,68 @@ | ||
| 1 | +# MacLustr Tunnel — passerelles publiques OVH pour le cluster (remplace ngrok) | |
| 2 | + | |
| 3 | +> ⚠️ **2026-10-02 : la passerelle secondaire R9128 (Gravelines) a été retirée** (serveur vidé, à résilier) ; **BHS64 est la seule passerelle**. L'interface `wg0` (10.66.0.x) et le LaunchDaemon `io.maclustr.wireguard` ont été supprimés de M1M32, M3U96a et M4M64b ; `mlt -g R9128` et la passerelle R9128 de `mld/config.py` n'existent plus ; les routes des sites hébergés sur les serveurs OVH retirés (websensor, market-atlas, hfmarketdata, cancerindex, company-atlas, satelliteindex, internetpressure, datacenterindex) ont été retirées (30 routes restantes). Les mentions de R9128, `wg0`, 10.66.0.x et 51.255.75.61 ci-dessous sont **historiques**. | |
| 4 | + | |
| 5 | +Mis en place le **2026-09-09** (R9128, France) puis **2026-09-10** (BHS64, Québec — passerelle principale). Premier site migré : **www.qc26.ca** (M3U96a:8200). | |
| 6 | + | |
| 7 | +``` | |
| 8 | +Internet → DNS (A → 51.161.112.61) → BHS64 (OVH Beauharnois) : Caddy 2 (TLS Let's Encrypt auto, :80/:443) | |
| 9 | + └─ WireGuard hub 10.67.0.1 (udp 51820) ─→ Macs, interface wg1 (10.67.0.x) | |
| 10 | +Internet → DNS (A → 51.255.75.61) → R9128 (OVH Gravelines) : idem, hub 10.66.0.1 ─→ Macs, interface wg0 (10.66.0.x) [secours / Europe] | |
| 11 | +``` | |
| 12 | + | |
| 13 | +Deux passerelles indépendantes : chaque Mac a deux interfaces WireGuard (`wg0` → France, `wg1` → Québec, LaunchDaemons `io.maclustr.wireguard` et `io.maclustr.wireguard-wg1`). Le DNS d'un site décide de la passerelle utilisée ; une route peut exister sur les deux (les certificats Let's Encrypt peuvent être copiés d'une passerelle à l'autre : `tar` de `/var/lib/caddy/.local`). `mlt` vise BHS64 par défaut, `mlt -g R9128 …` pour la France. Clés publiques des hubs : `maclustr-tunnel/hub-BHS64.pub`, `hub-R9128.pub`. Nouvelle passerelle = `gateway-setup.sh <alias> <ip> <sous-réseau>`. | |
| 14 | + | |
| 15 | +## Composants | |
| 16 | + | |
| 17 | +| Où | Quoi | | |
| 18 | +|---|---| | |
| 19 | +| R9128 | `wg-quick@wg0` (10.66.0.1/24, clé `/etc/wireguard/server.key`, pairs dans `/etc/wireguard/peers.d/*.conf`), `caddy` (Caddyfile `/etc/caddy/Caddyfile` + `import sites/*.caddy`), `ufw` (22, 80, 443 tcp/udp, 51820 udp), **`/usr/local/bin/tunnelctl`**, carte alias→IP `/etc/maclustr-tunnel/ipmap` | | |
| 20 | +| Macs | `wireguard-tools` + `wireguard-go` (Homebrew), conf `/opt/homebrew/etc/wireguard/wg0.conf` (Address 10.66.0.X/24, MTU 1380, AllowedIPs 10.66.0.0/24, PersistentKeepalive 25), LaunchDaemon `io.maclustr.wireguard` (boucle : remonte wg0 si absent, log `/var/log/maclustr-wireguard.log`) | | |
| 21 | +| Laptop | `~/Desktop/cluster-skill/mlt` (wrapper) + `maclustr-tunnel/` (tunnelctl, ipmap, Caddyfile, wg-node-setup.sh — copies de référence) | | |
| 22 | + | |
| 23 | +Plage **10.66.0.0/24** (pas 10.100.x : LAN des Macs loués Macly). Attribution fixe dans `ipmap` : M1M32 .2, M3U96a .10, M3U96b .11, M2U64 .12, M4M64a .13, M4M64b .14, … loués .40–.46. | |
| 24 | + | |
| 25 | +## Commandes | |
| 26 | + | |
| 27 | +```bash | |
| 28 | +mlt status # pairs (handshake, trafic) + routes | |
| 29 | +mlt peer M4M64a M2U64 # raccorder des Macs (idempotent) | |
| 30 | +mlt add www.exemple.com M4M64a:8250 # exposer un port → https://www.exemple.com (TLS auto) | |
| 31 | +mlt add api.exemple.com M4M64a:8251 M4M64b:8251 # ≥2 upstreams = load balancing (first + health /) | |
| 32 | +mlt redirect exemple.com www.exemple.com | |
| 33 | +mlt rm www.exemple.com | |
| 34 | +mlt ls | |
| 35 | +``` | |
| 36 | + | |
| 37 | +Sur la passerelle directement : `sudo tunnelctl …` (mêmes sous-commandes ; `tunnelctl peer add <alias> <pubkey>`). | |
| 38 | + | |
| 39 | +## État au 2026-09-10 : migration complète | |
| 40 | + | |
| 41 | +Les **34 sites** publics du cluster (33 + qc26) passent par BHS64 ; ngrok n'est plus utilisé pour eux (processus PM2 `*-ngrok` supprimés, LaunchAgents `io.adminka.ngrok` / `com.ka{2,4,6}.ngrok` déchargés et renommés `.disabled-ngrok-<date>`, manifestes mld `ngrok: null` + champ `tunnel`, sauvegardes `.bak-ngrok-<date>` dans `M1M32:~/dispatch/apps/`). Liste de référence : `maclustr-tunnel/sites-ngrok.txt` (domaine → nœud:port) et `ngrok-map.txt`. | |
| 42 | + | |
| 43 | +Outils de migration en lot (dans `maclustr-tunnel/`) : | |
| 44 | +- `migrate-watch.sh <sites.txt> [BHS64] [durée]` : surveille le DNS autoritaire de chaque domaine ; dès qu'il pointe vers la passerelle, `mlt add` (+ redirect apex si l'apex pointe aussi) puis test HTTPS. Journal `migrate.log`. | |
| 45 | +- `ngrok-retire.sh <ngrok-map.txt> <sites.txt> [BHS64] [durée]` : quand 8.8.8.8 et 1.1.1.1 renvoient la passerelle, supprime le tunnel ngrok (pm2 ou launchd) et met le manifeste mld à jour. Journal `ngrok-retire.log`. | |
| 46 | +- `wg-forward-setup.sh <ip-wg> <sudo> <port>…` : relais socat (LaunchDaemon `io.maclustr.wgfwd`, conf `/opt/homebrew/etc/maclustr-wgfwd.conf`) pour les apps qui n'écoutent que sur 127.0.0.1 (spbgit :7420 sur M1M32, api-ka :8000 sur M4M64b). | |
| 47 | +- Apex : les redirections GoDaddy existantes (`15.197.225.128`, `3.33.130.190`, `76.223.105.230` → www) ont été conservées ; seul qc26.ca a son apex sur la passerelle (redirect Caddy). | |
| 48 | + | |
| 49 | +## Migrer un site ngrok → tunnel (procédure suivie pour qc26) | |
| 50 | + | |
| 51 | +1. `mlt peer <nœud>` si le nœud n'est pas encore raccordé (`mlt status` : handshake récent, `ping 10.66.0.X` depuis R9128). | |
| 52 | +2. Vérifier l'app à travers le tunnel depuis R9128 : `curl -H "Host: www.site" http://10.66.0.X:PORT/`. | |
| 53 | +3. (Optionnel) répétition générale sur un hôte jetable : `mlt add test-x.51-255-75-61.sslip.io <nœud>:<port>` → https OK + cert Let's Encrypt. | |
| 54 | +4. GoDaddy : supprimer le `CNAME www → …ngrok-cname.com`, ajouter `A www → 51.255.75.61` (et `A @ → 51.255.75.61` pour l'apex). | |
| 55 | +5. Dès que le serveur autoritaire répond (`dig @ns5x.domaincontrol.com`), `mlt add www.site <nœud>:<port>` puis `mlt redirect site www.site`. Let's Encrypt lit le DNS autoritaire : pas besoin d'attendre les caches publics. | |
| 56 | +6. Vérifier : `curl --resolve www.site:443:51.255.75.61 https://www.site/`, cert, redirections 80→443 et apex, API. | |
| 57 | +7. Quand les caches publics ont basculé (ancien TTL, ~1 h) : `pm2 delete <app>-ngrok && pm2 save` sur le nœud, et dans le manifeste mld (`M1M32:~/dispatch/apps/<app>.json`) mettre `"ngrok": null` + `"tunnel": {…}` (sinon `mld deploy` relancerait ngrok). | |
| 58 | + | |
| 59 | +## Points d'attention | |
| 60 | + | |
| 61 | +- **Latence** : R9128 est à Gravelines (France) ; depuis le Québec, chaque requête fait Québec → France → Québec (+~200-250 ms vs ngrok US). Un serveur OVH **BHS (Beauharnois, Québec)** comme passerelle ramènerait ça à ~10 ms — à envisager si les sites sont surtout consultés au Québec. | |
| 62 | +- Caddy journalise dans journald (`journalctl -u caddy -f`) ; hôte inconnu → 404 « MacLustr Tunnel ». Certificats dans `/var/lib/caddy`. | |
| 63 | +- `wg show wg0` ne marche pas sur macOS (wireguard-tools 1.0.2026) : utiliser `wg show $(cat /var/run/wireguard/wg0.name)` en root. | |
| 64 | +- `sudo -S` lit le mot de passe sur stdin : ne jamais l'enchaîner avec un heredoc (bug corrigé dans wg-node-setup.sh). | |
| 65 | +- Le LaunchDaemon macOS peut refuser `bootstrap` juste après un `bootout` (EIO) : le script réessaie 6× puis fait `wg-quick up` directement. | |
| 66 | +- Migration ngrok → tunnel : **un site à la fois**, en gardant ngrok actif jusqu'à expiration des caches DNS. | |
| 67 | +- **Intégré à mld depuis le 2026-09-10 soir** (`mld/tunnel.py`) : `mld deploy`/`move` posent/repointent la route `https://<tunnel.domain> → <nœud courant>:<port>` via `tunnelctl` (ssh direct `ubuntu@51.161.112.61` avec la clé de M1M32), raccordent le nœud en wg1 s'il ne l'est pas (`assets/wg-node-setup.sh`), `mld retire` retire la route, `mld tunnel status|peer|route|rm` depuis le laptop, `mld heal` vérifie toutes les 5 min que chaque route pointe vers le bon nœud et que wg1 est monté. `mlt` (laptop) reste utile pour les sites hors mld et les redirections d'apex. | |
| 68 | +- Pas encore fait : fail2ban/CrowdSec, WebSocket (Caddy le gère nativement, `tunnel.websocket: true` dans le manifeste ajoute `--websocket`, à vérifier au premier site qui en a besoin). | |
added
NODES.md
+107 −0
@@ -0,0 +1,107 @@ | ||
| 1 | +# Inventaire complet des machines — MacLustr (LAN) et OVH | |
| 2 | + | |
| 3 | +Document présentable dérivé (sans identifiants) : `INVENTAIRE-MACLUSTR.md` / `.pdf` (2026-09-10 soir, 37 machines). Mis à jour le **2026-10-02** (**retrait de 4 des 5 serveurs OVH** : R9128, BHS64b, BHS128, BHS128b — seule la passerelle BHS64 reste, voir §3), le **2026-09-28** (ajout de m6mb + m5ma ; IP publique du LAN désormais 142.170.25.2), le **2026-09-26** (ajout de M5M36 + m6ma) et le **2026-09-25** (ajout du Mac Studio M4M64c ; **retrait des 12 Macs loués** Macly/MacStadium/rentamac, voir §2) et le **2026-09-21** (ajout de 3 Mac mini locaux m4ml/m4mm/m4mn ; **toutes les IP LAN ont été renumérotées par le DHCP** le 2026-09-21 — colonne IP LAN = état live `mld discover` du 2026-09-21, les alias `*.maclustr.io` pointent sur Tailscale et restent valides). Source de vérité des inventaires : | |
| 4 | +`~/Desktop/Cluster/maclustr-dispatch/mld/config.py` (mld), `maclustr-ios-v2/agent/agentd.py` (agent), `maclustr-v2/Sources/Services/ClusterManager.swift` (app macOS), `~/.ssh/config` (alias laptop). | |
| 5 | + | |
| 6 | +**Totaux** : 29 Macs, tous sur le LAN = 401 cœurs / 1 068 Go RAM (les 12 Macs loués ont été retirés le 2026-09-25, §2), + **1 serveur Linux OVH** : BHS64 (16 t / 64 Go, Beauharnois) = passerelle MacLustr Tunnel. Les 4 autres (R9128 Gravelines, BHS64b, BHS128, BHS128b Beauharnois) ont été **retirés le 2026-10-02** (§3). | |
| 7 | +Au 2026-09-08 : 22 Macs en ligne, **4 hors ligne** (M4BP36, m2m16, M3BA24, m2m8a — éteints), OVH en ligne. | |
| 8 | + | |
| 9 | +--- | |
| 10 | + | |
| 11 | +## 1. Nœuds MacLustr sur le LAN (192.168.2.x, maclustr.io, Tailscale) | |
| 12 | + | |
| 13 | +Utilisateur `simon-pierreboucher`, sudo `492592`, home `/Users/simon-pierreboucher`, clés ed25519 sans mot de passe, SSH inter-nœuds via LAN uniquement (Tailscale bloqué par ACL). Découverte des IP en live (`mld discover`, marqueur `~/.maclustr-node`). | |
| 14 | + | |
| 15 | +| Alias | Modèle | Puce | Cœurs | RAM | Disque | IP LAN | macOS | Rôle | État 2026-09-08 | Apps mld | | |
| 16 | +|---|---|---|---|---|---|---|---|---|---|---| | |
| 17 | +| M3U96a | Mac Studio | M3 Ultra | 32 | 96 Go | 926 Go | 192.168.2.80 | 26.6 | worker (calcul + apps), passerelle LLM llm.maclustr.io:9000 | en ligne | admin-ka, fetcha, job-ka, polyllm, qc26, rareindex, sorti-ka, uqo-imm1033, vrai-prix | | |
| 18 | +| M3U96b | Mac Studio | M3 Ultra | 32 | 96 Go | 926 Go | 192.168.2.85 (via M3U96a depuis le laptop) | 26.4 | **réservé hfmarketdata** | en ligne | hfmarketdata | | |
| 19 | +| M2U64 | Mac Studio | M2 Ultra | 24 | 64 Go | 926 Go | 192.168.2.61 | 26.5 | worker, sans session graphique (LaunchDaemon) | en ligne | food-ka, maclustr-appmanager, maclustr-console, maclustr-nodepages, spb-cloud, spboucher-ai, uqo-eval, vquant, **company-atlas** (www.company-atlas.co, :8360/8361, Postgres companyatlas, 2026-09-12) | | |
| 20 | +| M4M64a | Mac Studio | M4 Max | 16 | 64 Go | 926 Go | 192.168.2.70 | 26.5.1 | worker + **maclustr-agentd :9210** + **www.maclustr.io** (site vitrine, pinné : loopback agentd) | en ligne | fabri-ka, ka-stats, lou-ka, maclustr-agentd, maclustr-www, pdb-api, uqo-chat | | |
| 21 | +| M4M64b | Mac Studio | M4 Max | 16 | 64 Go | 926 Go | 192.168.2.77 | 26.5.1 | worker | en ligne | api-ka, auto-ka, cancerindex, groupe-ka, immo-ka, uqo-imm1003, websensor | | |
| 22 | +| M4M64c | Mac Studio | M4 Max | 16 | 64 Go | 494 Go | 192.168.2.103 | 26.4 | worker libre (ajouté 2026-09-25 ; Tailscale 100.125.225.117, S/N JX0PHFQHF1, wg1 10.67.0.33 raccordé ; bootstrap + prepare + harden faits le 2026-09-25 ; DNS GoDaddy A → IP Tailscale) | en ligne | — | | |
| 23 | +| M5M36 | Mac Studio | M5 Max | 18 | 36 Go | 494 Go | 192.168.2.104 | 27.0 | worker libre (ajouté 2026-09-26 ; Tailscale 100.69.26.91, S/N LY5Q6FD2PL, wg1 10.67.0.34) | en ligne | — | | |
| 24 | +| M4BP48 | MacBook Pro | M4 Max | 16 | 48 Go | 926 Go | 192.168.2.59 | 26.5.1 | worker | en ligne | — | | |
| 25 | +| M4BP36 | MacBook Pro | M4 Max | 14 | 36 Go | — | 192.168.2.86 | — | worker | **hors ligne (éteint)** | — | | |
| 26 | +| M4M36 | Mac Studio | M4 Max | 14 | 36 Go | 460 Go | 192.168.2.67 | 26.5.2 | worker, gardiens Ka | en ligne | ka2, ka4, ka6 | | |
| 27 | +| M2M32 | Mac Studio | M2 Max | 12 | 32 Go | 460 Go | 192.168.2.78 | 26.5.1 | worker dédié CountryAtlas | en ligne | countryatlas (8290/8291) | | |
| 28 | +| M2M32b | Mac Studio | M2 Max | 12 | 32 Go | 460 Go | 192.168.2.69 | 26.5.1 | worker dédié **SatelliteIndex** (Postgres 17 + Redis Homebrew, wg1 10.67.0.19) | en ligne | satelliteindex (8310 web, 8311 api, scheduler) | | |
| 29 | +| M2M32c | Mac Studio | M2 Max | 12 | 32 Go | 460 Go | 192.168.2.76 | 26.5.1 | worker dédié **AI Atlas** (Postgres 17 + pgvector + Redis Homebrew, wg1 10.67.0.20) | en ligne | ai-atlas (8320 web, 8321 api, scheduler) | | |
| 30 | +| m4mc | Mac mini | M4 Pro | 12 | 24 Go | 460 Go | 192.168.2.75 | 26.5.1 | worker | en ligne | — | | |
| 31 | +| **M1M32** | Mac Studio | M1 Max | 10 | 32 Go | 460 Go | 192.168.2.72 | 26.5.1 | **passerelle** : spbgit (git.spboucher.ai) + orchestrateur `mld` ; aucune app | en ligne | spbgit | | |
| 32 | +| m4ma | Mac mini | M4 | 10 | 24 Go | 460 Go | 192.168.2.83 | 26.5.1 | worker | en ligne | — | | |
| 33 | +| m4mb | Mac mini | M4 | 10 | 16 Go | 228 Go | 192.168.2.74 | 26.5.1 | worker | en ligne | — | | |
| 34 | +| m4md | Mac mini | M4 | 10 | 16 Go | 228 Go | 192.168.2.62 | 26.6.2 | worker (ajouté 2026-09-04) | en ligne | — | | |
| 35 | +| m4ml | Mac mini | M4 | 10 | 16 Go | 228 Go | 192.168.2.63 | 26.7 | worker (ajouté 2026-09-21 ; Tailscale 100.70.177.46, S/N VLXC9XWJLY) | en ligne | — | | |
| 36 | +| m4mm | Mac mini | M4 Pro | 12 | 48 Go | 460 Go | 192.168.2.92 | 26.5.1 | worker (ajouté 2026-09-21 ; Tailscale 100.115.250.59, S/N DYH4X6RW3R, wg1 réservé 10.67.0.31) | en ligne | — | | |
| 37 | +| m4mn | Mac mini | M4 Pro | 12 | 48 Go | 460 Go | 192.168.2.93 | 26.5.1 | worker (ajouté 2026-09-21 ; Tailscale 100.95.88.71, S/N J322FLVRW6, wg1 réservé 10.67.0.32) | en ligne | — | | |
| 38 | +| m6ma | Mac mini | M6 | 12 | 24 Go | 494 Go | 192.168.2.105 | 27.0 | worker libre (ajouté 2026-09-26 ; Tailscale 100.83.195.67, S/N FQ452T6L2W, wg1 10.67.0.35) | en ligne | — | | |
| 39 | +| m6mb | Mac mini | M6 | 12 | 24 Go | 494 Go | 192.168.2.106 | 27.0 | worker libre (ajouté 2026-09-28 ; Tailscale 100.102.242.94, S/N QR7JC5WMYC, wg1 10.67.0.36) | en ligne | — | | |
| 40 | +| m5ma | Mac mini | M5 Pro | 15 | 24 Go | 494 Go | 192.168.2.112 | 27.0 | worker libre (ajouté 2026-09-28 ; Tailscale 100.117.72.79, S/N X6YL6N495W, wg1 10.67.0.37) | en ligne | — | | |
| 41 | +| m2m16 | Mac mini | M2 Pro | 10 | 16 Go | — | 192.168.2.65 | — | worker | **hors ligne (éteint)** | — | | |
| 42 | +| M3BA24 | MacBook Air | M3 | 8 | 24 Go | — | 192.168.2.81 | — | worker | **hors ligne (éteint)** | — | | |
| 43 | +| M3BA16 | MacBook Air | M3 | 8 | 16 Go | 460 Go | 192.168.2.64 | 26.5.1 | worker | en ligne | — | | |
| 44 | +| m2m8a | Mac mini | M2 | 8 | 8 Go | — | 192.168.2.66 | — | worker | **hors ligne (éteint)** | — | | |
| 45 | +| m2m8b | Mac mini | M2 | 8 | 8 Go | 228 Go | 192.168.2.68 | 26.5.1 | worker | en ligne | — | | |
| 46 | + | |
| 47 | +Sous-total LAN : 29 nœuds, 401 cœurs, 1 068 Go. Autres équipements LAN : NAS UGREEN DXP4800 Plus (192.168.2.173, `nas_clustr`) et DH4300 Plus (192.168.2.175, `nas2_clustr`). | |
| 48 | + | |
| 49 | +--- | |
| 50 | + | |
| 51 | +## 2. Macs dédiés loués — RETIRÉS du cluster le 2026-09-25 | |
| 52 | + | |
| 53 | +Douze Macs loués hors LAN avaient été ajoutés du 8 au 10 septembre 2026 (`REMOTE_NODES` + RESERVED dans mld, agentd, app macOS, pairs wg1 10.67.0.40–51 sur BHS64). **Tous retirés le 2026-09-25** à la demande de l'utilisateur (les apps dessus n'étaient pas importantes ; seule `llm-api` sur M1M64 tournait — `mld retire llm-api`, routes www.llm-api.io / llm-api.io supprimées). | |
| 54 | + | |
| 55 | +| Alias | Machine | Fournisseur / lieu | IP publique | Utilisateur | Nettoyage 2026-09-25 | | |
| 56 | +|---|---|---|---|---|---| | |
| 57 | +| M1M64 | Mac Studio M1 Max 10 c / 64 Go | Macly, Istanbul | 45.74.241.221 | simon | fait | | |
| 58 | +| m4mh, m1m16b (M1), m4mk | Mac mini M4 / M1 / M4 10-8-10 c / 16 Go | Macly, Istanbul | 45.74.241.182 / .171 / .98 | simon | fait | | |
| 59 | +| m4me, m4mf, m4mi, m4mj | Mac mini M4 10 c / 16 Go | Macly, Uşak | 45.74.245.36 / .37 / .39 / .38 | simon | fait | | |
| 60 | +| m2m16b | Mac mini M2 Pro 10 c / 16 Go | MacStadium, Atlanta | 208.52.154.68 | administrator | fait | | |
| 61 | +| m1m16, m2m16c | Mac mini M1 / M2 8 c / 16 Go | MacStadium, Dublin | 207.254.29.71 / .128 | administrator | fait | | |
| 62 | +| m4mg | Mac mini M4 10 c / 16 Go | rentamac, Limassol (NAT, gate1.rentamac.io:27335) | — | rentamac | **injoignable** (passerelle SSH fermée) — rien nettoyé sur la machine | | |
| 63 | + | |
| 64 | +Nettoyage effectué sur chaque Mac joignable : `pm2 kill` + `pm2 unstartup`, LaunchDaemons `pm2.<user>.plist` et `io.maclustr.wireguard-wg1.plist` retirés, `wg-quick down wg1` + `/opt/homebrew/etc/wireguard` effacé, `~/apps`, `~/.pm2`, `~/.maclustr-node` supprimés, clés `maclustr-cluster`/`maclustr-m1m32`/`maclustr-agentd` retirées d'`authorized_keys` (plus aucun accès SSH depuis le laptop ; Homebrew et les comptes fournisseur laissés en place). Côté cluster : `mld/config.py` (NODES, REMOTE_NODES vide, RESERVED — commit f7bd29d), `M1M32:~/dispatch/.sudo-<alias>` effacés, `agentd.py` + `ClusterManager.swift` (REMOTE_NODES vide, agentd redéployé → 25 nœuds), BHS64 `tunnelctl peer rm` ×12 + ipmap purgée (sauvegarde `ipmap.bak-20260925`), alias `Host gate1.rentamac.io` retirés de M1M32 et M4M64a, blocs `~/.ssh/config` du laptop supprimés (copie `~/.ssh/config.bak-20260925-loues`). Les mots de passe sudo et identifiants DeskIn de ces machines ne sont plus consignés ici. | |
| 65 | + | |
| 66 | +--- | |
| 67 | + | |
| 68 | +## 2b. Nœuds mobiles (iPhone / iPad) — depuis le 2026-09-28 | |
| 69 | + | |
| 70 | +Les iPhone/iPad qui ont rejoint le cluster via l'app MacLustr iOS ≥ 6.0 (« Ajouter cet appareil au cluster ») sont des **nœuds de plein droit** : alias (ex. `iph17pm`), épinglés ou non, métriques (CPU, mémoire, batterie, thermique, réseau) historisées 24 h, jobs `ping` / `sys.info` / `http.fetch` / `js.run` / `compute.bench` / `net.probe`. Ils *tirent* leur travail depuis le coordinateur **maclustr-mobile 1.1.0** (M4M64a:9320, Tailscale, pas de SSH, actifs seulement app ouverte) ; **agentd ≥ 3.1.0** les expose dans `/api/cluster` (`kind: mobile`), `/api/summary.mobiles`, les incidents et les proxys `/api/mobile/*`. Inventaire live : `mlmobile nodes` ou l'onglet Flotte mobile des apps. Appareils connus au 2026-09-28 : 3 × iPhone 17 Pro Max (un seul enrôlé, `iphone`, en attente de son alias via l'app 6.0). | |
| 71 | + | |
| 72 | +## 3. Serveurs dédiés OVHcloud (Linux) | |
| 73 | + | |
| 74 | +| Alias | Nom OVH | Matériel | IP | OS | Utilisateur | Emplacement | État | | |
| 75 | +|---|---|---|---|---|---|---|---| | |
| 76 | +| **BHS64** (`ovh2`) | `ns5044661.ip-51-161-112.net` (ADVANCE-2, ID 540625) | AMD EPYC 4345P 8 c/16 t 3,8–5,5 GHz, 64 Go DDR5 ECC 3600, 2×960 Go NVMe Samsung en **RAID 1 logiciel** (md2 `/boot`, md3 `/` 878 Go), 3 Gbit/s public, 25 Gbit/s privé | **51.161.112.61** (gw 100.64.0.1 DHCP), IPv6 `2607:5300:22a:3d00::/56` | Ubuntu Server **24.04.4 LTS**, hostname `bhs64`, TZ America/Toronto | `ubuntu` (sudo sans mot de passe) ; root joignable par clé | **Beauharnois BHS8** (Québec), baie BHS0805A03C, `ca-east-bhs-a` | en ligne (installé 2026-09-10) — **passerelle MacLustr Tunnel principale** (hub WireGuard 10.67.0.1, Caddy, ufw, tunnelctl) | | |
| 77 | +| ~~**BHS64b**~~ (`ovh3`, **RETIRÉ le 2026-10-02**) | `ns5015175.ip-51-161-112.net` (ADVANCE-2) | jumeau de BHS64 : AMD EPYC 4345P 8 c/16 t, 64 Go DDR5 ECC, 2×960 Go NVMe en **RAID 1 logiciel**, 3 Gbit/s | **51.161.112.66**, IPv6 `2607:5300:22a:4200::/56` | Ubuntu Server **24.04.4 LTS**, hostname `bhs64b`, TZ America/Toronto | `ubuntu` (sudo sans mot de passe) ; root joignable par clé | Beauharnois, `ca-east-bhs-a` | en ligne (installé 2026-09-10 soir, même recette Safari) — **rôles (2026-09-14)** : worker de crawl DataCenterIndex (Docker `dci-worker-1`, `dci0` 10.68.0.2) ; **InternetPressure.io y a tourné du 2026-09-12 au 2026-09-14** puis a déménagé sur BHS128b (pile en `compose down`, volumes `ip_*` 6,3 Go gardés en copie à froid, wg1 10.67.0.61 toujours raccordé) ; pas une passerelle tunnel | | |
| 78 | +| ~~**BHS128**~~ (`ovh4`, **RETIRÉ le 2026-10-02**) | `ns5044426.ip-51-161-112.net` (ADVANCE-1, ID 528691) | AMD EPYC 4245P 6 c/12 t 3,9–5,4 GHz, 128 Go DDR5 3600, 2×960 Go NVMe en **RAID 1 logiciel**, 3 Gbit/s public, 25 Gbit/s privé | **51.161.112.69** (gw 100.64.0.1 DHCP), IPv6 `2607:5300:22a:4500::/56` | Ubuntu Server **24.04 LTS**, hostname `bhs128`, TZ America/Toronto | `ubuntu` (sudo sans mot de passe) ; root joignable par clé | Beauharnois BHS8, baie BHS0805A01C, `ca-east-bhs-a` | en ligne (installé 2026-09-10 soir, recette Safari `cluster-skill/ovh-manager/`) — **rôles (2026-09-13)** : DataCenterIndex data (Docker, `dci-*`, edge :8300 sur wg1 10.67.0.60) + **WebSensor** (`~/apps/websensor`, PM2 natif `websensor-engine/factory/api/web`, :8260, Postgres 17 natif base `websensor` C.UTF-8 + Redis, blobs `~/websensor-data/blobs`) + **Market Atlas** (`~/apps/market-atlas`, PM2 `market-atlas-core/web`, :8380/:8382, base `market_atlas`, données `~/market-atlas-data`) migrés depuis M4M64b/M2U64 le 2026-09-13 soir (routes BHS64 `www.websensor.io → 10.67.0.60:8260`, `www.market-atlas.co → 10.67.0.60:8380`) + **hfmarketdata** (`~/hfmarketdata`, venv Python 3.14 uv, PM2 `hfmarketdata-api` uvicorn :8090, `edgar-incremental`, crons frd-refresh/contracts-backfill/edgar-reconcile ; lac FirstRate `~/firstratedata` 359 Go Parquet + state/edgar copié depuis M3U96b le 2026-09-14 ; route `www.hfmarketdata.io → 10.67.0.60:8090`). **Toutes les copies Mac de ces trois apps ont été effacées le 2026-09-14** (M4M64b, M2U64, M3U96b : code, données, bases, Redis, logs ; M1M32 : staging + manifestes, sauvegardés sur le laptop `~/Desktop/Cluster/secrets/mld-manifests-retired-20260914/`). Node 22 (nodesource) + pnpm 11.1.2 + PM2 (systemd `pm2-ubuntu`, LimitNOFILE 1 048 576), configs `~/apps/pm2.<app>.config.cjs` générées par `~/apps/.manifests/gen-pm2.py` depuis les manifestes mld (secrets, 0600). Hors mld/agentd, pas une passerelle tunnel — ⚠️ **incident 2026-09-25** : disque plein depuis le 17 sept. (journal Postgres 410 Go, bug bigint market-atlas-core), PM2 + Postgres KO, sites 502 ; nettoyé (429 Go libres), conf.d quiet + logrotate 2G, réserve ext4 1 % | | |
| 79 | +| ~~**BHS128b**~~ (`ovh5`, **RETIRÉ le 2026-10-02**) | `ns5035327.ip-51-161-112.net` (ADVANCE-2, ID 503587) | AMD EPYC 4344P 8 c/16 t, **128 Go** DDR5, 2×960 Go NVMe Samsung PM9A3 en **RAID 1 logiciel** (md3 878 Go), 1 Gbit/s sortant / 25 Gbit/s entrant public, 25 Gbit/s privé | **51.161.112.85** (gw 100.64.0.1), IPv6 `2607:5300:22a:5500::/56` (sortante OK) | Ubuntu Server **24.04 LTS**, hostname `bhs128b` (installé sous `bhs64c` puis renommé après `free -g`), TZ America/Toronto | `ubuntu` (sudo sans mot de passe) ; root joignable par clé ; clés maclustr-cluster + maclustr-m1m32 + maclustr-agentd ; ufw actif (22) | Beauharnois BHS8, baie BHS0803A03C, `ca-east-bhs-a` | en ligne (installé 2026-09-14 ~02:58 via la recette Safari `cluster-skill/ovh-manager/`, marqueur `/root/maclustr-postinstall.done`) — **rôles (2026-09-14 matin, migrés le jour même)** : **cancerindex** (`~/apps/cancerindex`, PM2 web :8250 / api :8251 / worker / backup cron 05:20, Postgres `cancerindex` pgvector 2,6 Go, ex-M4M64b), **company-atlas** (`~/apps/company-atlas`, venv uv 3.12, PM2 api :8361 / scheduler / web :8360, Postgres `companyatlas` 1 Go + `~/company-atlas-data` 7 Go, ex-M2U64), **satelliteindex** (`~/apps/satelliteindex`, venv uv 3.12, PM2 api :8311 / scheduler / web :8310, Postgres `satelliteindex`, Redis db 4, `~/satelliteindex-data`, ex-M2M32b), ~~rareindex~~ (**retirée le 2026-09-26**, disque plein : base 351 Go + images 299 Go effacées, code final sur spbgit 8fd0330, routes retirées → 647 Go libres), **InternetPressure** (Docker Compose projet `ip`, `/opt/internetpressure`, edge :8350 lié à wg1 **10.67.0.62**, volumes `ip_*` copiés de BHS64b, sauvegarde cron 04:10 UTC → BHS128:/srv/backups). Routes BHS64 : `www.cancerindex.io → 10.67.0.62:8250`, `www.company-atlas.co → :8360`, `www.satelliteindex.io → :8310`, `www.rareindex.io → :8210`, `www.internetpressure.io → :8350`. Node 22 + pnpm 11.1.2 + PM2 (systemd `pm2-ubuntu`, LimitNOFILE 1 048 576) + Postgres 17.11 PGDG + pgvector + Redis + Docker 29 + uv ; configs `~/apps/pm2.<app>.config.cjs` régénérables par `~/apps/.manifests/gen-pm2.py` (manifestes mld avec secrets, 0600) ; ufw wg1 ouvert depuis 10.67.0.1 sur 8210/8250/8310/8350/8360. Disque : **incident 2026-09-25** — plein depuis le 19 sept. (rareindex images 299 Go + base 303 Go), PM2 orphelin + Postgres KO, sites 502 ; relancé, dumps `/srv/migrate` supprimés, réserve ext4 1 % → **49 Go libres seulement (95 %)**, rareindex doit déménager ou être borné. Copies d'origine conservées à froid (retirées de mld avec `--keep-dir` ; pile IP de BHS64b en `compose down`, volumes gardés). Pas une passerelle tunnel, hors mld/agentd | | |
| 80 | +| ~~**R9128**~~ (`ovh1`, **RETIRÉ le 2026-10-02**) | `ns3049362.ip-51-255-75.eu` (RISE-L, ID 1907852) | AMD Ryzen 9 9950X 16 c/32 t 4,3–5,7 GHz, 128 Go DDR5 3600, 2×960 Go NVMe en **RAID 1 logiciel** (md2 `/boot` 1 Gio, md3 `/` 878 Go, swap 2×512 Mio), 1 Gbit/s public + privé | **51.255.75.61** (gw 51.255.75.254), IPv6 `2001:41d0:1004:273d::/64` | Ubuntu Server **24.04.4 LTS**, hostname `r9128`, TZ America/Toronto | `ubuntu` (sudo sans mot de passe) ; root joignable par clé | Gravelines GRA1, baie G118A10, `eu-west-gra-a` | en ligne (installé 2026-09-08) | | |
| 81 | + | |
| 82 | +**Retrait du 2026-10-02 (demande : ne garder qu'une seule instance OVH, la passerelle tunnel de Beauharnois).** R9128, BHS64b, BHS128 et BHS128b ont été retirés du MacLustr : | |
| 83 | +- Apps arrêtées et **effacées** (code, bases, données, Docker) : WebSensor, Market Atlas, hfmarketdata (lac FirstRate 359 Go Parquet — regénérable par `frd_downloader`), DataCenterIndex (data BHS128 + crawl BHS64b), cancerindex, company-atlas, satelliteindex, InternetPressure (+ sondes `ip-probe` R9128/BHS128). Sites publics correspondants retirés de Caddy (BHS64 passe de 40 à 30 routes) ; les DNS GoDaddy `A → 51.161.112.61` sont restés en place (404 « MacLustr Tunnel »). | |
| 84 | +- **Code final sur spbgit** (commit `retire: production shut down 2026-10-02` dans chaque dépôt ; `datacenterindex.git` créé ce jour-là, il n'avait pas de remote). Les copies déployées étaient à jour par rapport à `main` (seuls `requirements-prod-freeze.txt` et les `allowBuilds` pnpm ont été récupérés). | |
| 85 | +- **Sauvegardes à froid** : `M3U96b:~/ovh-retired-20261002/{BHS128,BHS128b,BHS64b}/` (dumps `pg_dump -Fc` de market_atlas, websensor, cancerindex, companyatlas, satelliteindex, Postgres Docker DCI + IP, état hfmarketdata `state/hfmd.db` + meta + edgar, archives de sauvegardes DCI/IP/cancerindex, satelliteindex-data) ; manifestes mld, configs PM2, `.env`, compose, `probe.yaml`, Caddyfile R9128 sur le laptop `~/Desktop/Cluster/secrets/ovh-retired-20261002/` (0600). **Non sauvegardés** (volumineux, regénérables) : Parquet FirstRate 359 Go, `company-atlas-data/objects` 56 Go, blobs websensor 14 Go, raw market-atlas 27 Go, ClickHouse DCI/IP, `satelliteindex/apps` 132 Go. | |
| 86 | +- Infra : pairs wg1 BHS64b/BHS128/BHS128b retirés du hub BHS64 ; interface **wg0 (R9128) retirée** de M1M32, M3U96a, M4M64b (LaunchDaemon `io.maclustr.wireguard` supprimé) ; `mld` 5eaeaf7 (passerelle R9128 retirée de `config.py`), **agentd 3.1.2** (SERVERS = BHS64 seul, sonde egress R9128 retirée), `mlt` sans `-g R9128` ; alias SSH `R9128/ovh1`, `BHS64b/ovh3`, `BHS128/ovh4`, `BHS128b/ovh5` retirés du laptop et de M1M32 (sauvegardes `~/.ssh/config.bak-20261002-ovh`). | |
| 87 | +- Serveurs vidés (services désactivés, données effacées, OS de base conservé, accès `ubuntu` toujours possible par IP) — **la résiliation reste à faire dans le manager OVH** (Bare Metal Cloud → Serveurs dédiés → « Résilier ») : R9128 ID 1907852 (renouvellement 8 oct. 2026), BHS128 ID 528691, BHS128b ID 503587, BHS64b (`ns5015175`). | |
| 88 | + | |
| 89 | +- Clés autorisées (`ubuntu` et `root`) : `maclustr-cluster`, `maclustr-m1m32`, `maclustr-agentd` — testées depuis le laptop, M1M32 et M4M64a. Alias `BHS64` aussi dans `~/.ssh/config` de M1M32 (les alias des serveurs retirés ont été supprimés le 2026-10-02). | |
| 90 | +- **Recette d'installation OVH sauvegardée** dans `~/Desktop/cluster-skill/ovh-manager/` (`runjs.sh` = `osascript -l JavaScript` → `Safari.doJavaScript` dans l'onglet courant ; `install.js` → `os.js` → `next.js` → `fill-<alias>.js` → `valider.js` → `prog.js` ; `postinstall.sh` = script post-install commun ; `wait-<alias>.sh` = attente fin d'install + SSH). Safari doit être connecté au manager (`https://manager.ca.ovhcloud.com`) et « Autoriser JavaScript des événements Apple » activé. Le contenu du serveur est dans l'`iframe` (même origine, `contentDocument`). | |
| 91 | +- ⚠️ Clé `maclustr-agentd` sur les **Macs** : elle doit être dans `~/.ssh/authorized_keys` de chaque nœud LAN, sinon l'agent (et les apps macOS/iOS) le montre « désactivé » (cas de M3U96a, m2m8a, m2m16, M3BA24 corrigé le 2026-09-10). `mld prepare` ne la pose pas. | |
| 92 | +- Installé depuis https://manager.ca.ovhcloud.com (Bare Metal Cloud → Serveurs dédiés → bloc OS → « Installer mon serveur ») avec script post-installation (clés, apt : git, python3, htop, tmux, jq, rsync, build-essential, unattended-upgrades). Marqueur `/root/maclustr-postinstall.done`. | |
| 93 | +- Renouvellement automatique : BHS64 le 10 octobre 2026 (sans engagement), monitoring OVH avec intervention proactive. R9128 (renouvellement 8 octobre 2026) est à résilier avant cette date. | |
| 94 | +- BHS64 installé le 2026-09-10 par la même recette (manager OVH piloté en Safari `do JavaScript` : lien `#/dedicated/server/<nom>`, bouton « Installer mon serveur », Ubuntu 24.04, étape 4 hostname/sshKey/postInstallationScript, ~20 min) puis `maclustr-tunnel/gateway-setup.sh BHS64 51.161.112.61 10.67.0`. Latence depuis Saint-Augustin ≈ 10 ms (vs ≈ 85 ms pour R9128) : c'est la passerelle de `mlt` ; R9128 (ex-secours) a été retirée le 2026-10-02. | |
| 95 | +- **Hors mld / agentd** (Linux : pas de Homebrew, PM2 launchd, ni sondes macOS) → piloter en SSH direct ; pare-feu `ufw` inactif (à activer si exposé). | |
| 96 | +- **Rôle depuis le 2026-09-09 soir : passerelle publique « MacLustr Tunnel »** (WireGuard hub 10.66.0.1 udp 51820 + Caddy TLS auto + ufw, outil `tunnelctl`) qui remplace ngrok site par site — voir `MACLUSTR-TUNNEL.md`. Premier site : www.qc26.ca → M3U96a:8200. SPB AI (API LLM llama.cpp + passerelle, ai.spboucher.com) y a tourné dans la journée du 2026-09-09 puis a été retirée et effacée (`~/llm` supprimé, unités `spb-*` retirées) pour libérer le serveur ; source laptop `~/Desktop/Cluster/spb-ai`. Restent : build-essential, cmake, ngrok (authtoken cluster dans `~/.config/ngrok/ngrok.yml`). | |
| 97 | + | |
| 98 | +--- | |
| 99 | + | |
| 100 | +## 4. Accès et outils | |
| 101 | + | |
| 102 | +- `ssh <alias>` depuis le laptop pour toutes les machines (utilisateur géré par `~/.ssh/config`). | |
| 103 | +- `mld scan` / `mld nodes` (état live des 26 Macs), `mld status` (apps), `mld deploy <app> [--node X]`. | |
| 104 | +- **Vitrine publique : https://www.maclustr.io** (app mld `maclustr-www`, M4M64a:8280 ; inventaire dans `~/Desktop/Cluster/maclustr-www/public/data.js` — à mettre à jour avec ce fichier à chaque ajout/retrait de machine). | |
| 105 | +- Monitoring : maclustr-agentd (M4M64a:9210) → app macOS **MacLustr 3.0.3** (`/Applications`) et app iOS ; les 4 nœuds éteints y apparaissent « offline ». | |
| 106 | +- LLM cluster : https://llm.maclustr.io/v1 (M3U96a:9000). | |
| 107 | +- Pour ajouter un nœud loué : suivre la section « Nœud hors LAN » du README de maclustr-dispatch, puis mettre à jour les trois inventaires (mld, agentd, ClusterManager.swift) et ce fichier. | |
added
NODES.pdf
+0 −0
Binary file not shown.
added
cluster-deployments.json
+761 −0
@@ -0,0 +1,761 @@ | ||
| 1 | +{ | |
| 2 | + "updated": "2026-09-04", | |
| 3 | + "deployments": [ | |
| 4 | + { | |
| 5 | + "app": "ka-stats", | |
| 6 | + "node": "M1M32", | |
| 7 | + "port": 8140, | |
| 8 | + "domain": "www.ka-stats.com", | |
| 9 | + "process_manager": "pm2", | |
| 10 | + "label_or_name": "ka-stats + ka-stats-ngrok", | |
| 11 | + "dir": "~/apps/ka-stats", | |
| 12 | + "start": "cd ~/apps/ka-stats && pm2 start /opt/homebrew/bin/node --name ka-stats --interpreter none -- server.mjs 8140 ; pm2 start /opt/homebrew/bin/ngrok --name ka-stats-ngrok --interpreter none -- http --url=www.ka-stats.com 8140", | |
| 13 | + "deployed": "2026-08-24", | |
| 14 | + "notes": "Ka·Stats — l'explorateur de statistiques du Québec (Groupe KA), EN LIGNE sur https://www.ka-stats.com (bout-en-bout validé 2026-08-24). RENOMMÉ depuis qc-stats le soir même : l'utilisateur a finalement acheté ka-stats.com (GoDaddy, CNAME www → ngrok déjà posé) — marque Ka·Stats, dir/pm2/fichiers renommés. Serveur Node ZÉRO dépendance : agrège en direct les /api/stats/dashboard (contrat ka-stats v2) des 12 plateformes publiques (enveloppe api-ka {success,data} dépliée), cache mémoire+disque 10 min avec stale-while-error, endpoints /api/sites /api/overview /api/dashboard/:id, healthcheck /healthz. Front SPA vanilla ka-ui (tokens.css vendorisé + fonts Space Grotesk/JetBrains Mono, accent bleu Québec #095797) : Vue d'ensemble (héros 4,78 M d'éléments, pouls de l'écosystème, grille 12 plateformes, prix du Québec, records), /site/:id (rendu générique COMPLET du contrat v2 : KPI+sparklines, jauges, séries crosshair+infobulles, multi-courbes à motifs de trait, empilées, anneaux, histogrammes, géo, heatmaps calendrier+horaire, tableaux triables/filtrables/paginés, records, menu des 5 rapports PDF du satellite), /indicateurs (par thème), /comparer (≤4 plateformes, indice base 100, un seul axe), /palmares. Graphiques QCharts (public/charts.js) conformes au skill dataviz : palette catégorielle validée, vue tableau jumelle sur chaque graphique, gaps de surface 2 px, grilles hairline. | 2026-08-24 v2 INTERACTIF+STUDIO : lineChart interactif (zoom par sélection/brush, double-clic reset, légende cliquable pour masquer des séries), commutateur de type sur chaque carte (Ligne/Aire/Barres, Barres/Anneau), exports ⭳CSV/⭳PNG (SVG→canvas 2×, styles inlinés) ; nouvelle page /studio = CONSTRUCTEUR D'INDICATEURS : endpoints /api/catalog (227 métriques aplaties des 12 dashboards : kpi|series|multi|stacked, réf `site|kind|id[|sub]`) + /api/metric?ref=, picker avec recherche (≤4 métriques), transformations (brut/indice 100/variation %/moy. mobile 7 j/cumul), combinaisons A÷B A−B A+B (2 métriques), titre, sauvegarde localStorage (kastats_studio, 30 max) + LIEN PARTAGEABLE /studio?c=<json url-encodé> ; bouton « ✦ Studio » sur les graphiques des pages plateformes (préremplit la config). GOTCHA rsync : `-e \"$RSH\" $DEST:path` avec variables shell a interprété la destination comme CHEMIN LOCAL → dossier parasite M1M32pps créé dans le dossier SOURCE du laptop (rsync exit 0 !) puis resynchronisé sur le nœud — supprimé des deux côtés ; toujours vérifier le transfert avec -v/grep sur le nœud. | 2026-08-24 v2.1 SEO+BRANDING : metas SSR par route dans server.mjs (renderIndex : title/description/canonical/og:url/twitter + JSON-LD @graph Organization+WebSite+WebPage injectés dans index.html via marqueur <!--SEO--> ; routes /, /site/:id [titre+desc par plateforme], /indicateurs, /comparer, /studio, /palmares), og:site_name/locale/image:alt+dims, twitter summary_large_image ; BANNIÈRE OG officielle 1200×630 (gabarit gen-assets.mjs de ka-ui rendu via chrome-headless-shell — Playwright absent ; boxed « Stats » INVERSÉ bleu #095797/texte blanc et accents éclaircis #79b4e5 sur encre car le bleu Québec est trop sombre pour le gabarit standard) + favicon.svg/apple-touch-icon.png officiels « Ka » ; assets aussi déposés dans ka-ui/assets/ka-stats/ (canonique) ; sitemap 17 URLs (+/studio). GOTCHA données : vrai-prix publie une série ANNUELLE → le pouls n'agrège que les points datés au jour (≥8 pts, report dernière valeur, couverture ≥70 %) comme le hub. pm2 save + LaunchAgent pm2.simon-pierreboucher.plist installés (pm2 installé sur le nœud à cette occasion). NGROK : pattern standard (ngrok http --url=www.ka-stats.com 8140). LEÇON du soir : « réservé dans ngrok » ≠ domaine enregistré — qc-stats.com n'a jamais été acheté (RDAP Verisign 404), d'où des heures de HTTP 000 avant que le vrai domaine (ka-stats.com) soit nommé. ⚠️ SSH Tailscale laptop→M1M32 tombé pendant le déploiement : rebond LAN ssh -A -J M3U96a simon-pierreboucher@192.168.2.76 (M1M32 = .76, hostname MacStudnpierre8). Source laptop : ~/Desktop/Groupe-KA/apps-web/ka-stats. En-tête auteur Simon-Pierre Boucher — contact@spboucher.ai partout." | |
| 15 | + }, | |
| 16 | + { | |
| 17 | + "app": "spboucher-ai", | |
| 18 | + "node": "M2U64", | |
| 19 | + "port": 3100, | |
| 20 | + "domain": "www.spboucher.ai", | |
| 21 | + "process_manager": "pm2", | |
| 22 | + "label_or_name": "spboucher + spboucher-ngrok", | |
| 23 | + "dir": "~/spboucher-ai", | |
| 24 | + "start": "scripts/start-production.sh (next start -H 0.0.0.0 -p 3100) ; ngrok http --url=www.spboucher.ai 3100", | |
| 25 | + "deployed": "2026-08-12", | |
| 26 | + "notes": "New multi-page academic + Zyquo apps site (Next.js 16 App Router, Tailwind v4, Framer Motion). Rebuilt from scratch per new CLAUDE.md spec (pages: /, /research, /teaching, /apps, /cv). Port 3100 car 3000 = console.maclustr.io. Health: /api/health. Source: laptop ~/Desktop/spboucher. pm2 save fait. | 2026-08-05: MAJ v2 — page /apps avec section Web Platforms & Open Source (8 repos GitHub) + redesign visuel complet (pill nav flottante, cartes rounded-3xl, palette éclaircie). Rebuild + pm2 restart spboucher OK. | 2026-08-12: MAJ — /apps + pages détail pour Search-Box et WorthDoing (commit 1fff779 poussé sur git.spboucher.ai). Rebuild + pm2 restart spboucher OK. | 2026-08-28: nouvel onglet « Projects » (/projects) présentant l'écosystème Groupe Ka — lib/groupe-ka.ts (14 plateformes + hub, accents/taglines officiels tirés du SITES de ka-stats), components/ka-app-card.tsx, page groupée par catégories (Housing/Mobility & Employment/Consumer/Culture & Discovery/Data & Infrastructure), nav header+footer MAJ. Rebuild + pm2 restart spboucher OK, /projects 200 public. Le VRAI repo = copie laptop ~/Desktop/Projets/apps-web/spboucher (origin spbgit git.spboucher.ai/spboucher.ai.git + remote github) ; le dir du nœud n'est pas un repo git (rsync/scp depuis le laptop). Commit aa361ce poussé sur spbgit. | 2026-08-28 v2 : pages détail /projects/[slug] pour les 18 entrées Ka (contenu anglais détaillé tiré des README sur les nœuds — lib/groupe-ka-details.ts : hero, stats, features, stack, docUrl) + ka2/ka4/ka6 ajoutés (catégorie AI Explorer Bots, favicons ka*.bot). Commit poussé sur spbgit." | |
| 27 | + }, | |
| 28 | + { | |
| 29 | + "app": "lou-ka", | |
| 30 | + "node": "M3U96b", | |
| 31 | + "port": 8095, | |
| 32 | + "domain": "www.lou-ka.com", | |
| 33 | + "process_manager": "pm2", | |
| 34 | + "label_or_name": "lou-ka-web + lou-ka-sync + lou-ka-ngrok", | |
| 35 | + "dir": "~/apps/lou-ka", | |
| 36 | + "start": "pm2 start .venv/bin/python --name lou-ka-web --interpreter none --cwd ~/apps/lou-ka -- run.py serve 8095 ; pm2 start .venv/bin/python --name lou-ka-sync --interpreter none --cwd ~/apps/lou-ka -- run.py watch 60 ; pm2 start /opt/homebrew/bin/ngrok --name lou-ka-ngrok --interpreter none -- http --url=www.lou-ka.com 8095", | |
| 37 | + "deployed": "2026-08-15", | |
| 38 | + "notes": "2026-08-18 : qualité des données (score de complétude + quarantaine, audit d'images imgcheck, 26 connecteurs enrichis galeries+détails), mobile validé (matrice Playwright 360-1440 px, lightbox tactile, miniatures WebP /api/img, KA Agent plein écran), framework de juste valeur fairvalue.py (pastilles sous/dans/au-dessus du marché, filtre deal=sous, /juste-valeur, endpoint API-KA /api/v1/louka/fairvalue). Base en WAL. Commits dabf9c3 + 3e74b24. Agrégateur de logements QC/Lévis/Gd-Mtl (FastAPI+SQLite+React PWA). MAJ 2026-08-08 : refonte majeure — 82 connecteurs (13 nouveaux : Somex, Sila, Edifia, AULA, La Cour, Louis XIV, Mercini, Girescom, Mosaïque, SIG, Guillot, Forts, Centurion + Logis-Experts), schéma enrichi (dispo ISO, superficie, animaux, inclusions structurées), géocodage Nominatim+Adresses Québec (cache seedé, 97,5% GPS), carte MapLibre (vue Liste/Carte), tests fixtures (191). ~6000 annonces après premier cycle de sync. Source: laptop ~/Desktop/lou-ka + github spboucher-ai/lou-ka. | MAJ 2026-08-12 : vague « portails & courtiers location » — 10 connecteurs portés d'Immo-Ka (Kijiji c37+c36, LesPAC b457/458/460, RE/MAX Québec Meilisearch a-louer, Sutton typeInscription=2, Royal LePage transactionType=LEASE, Via Capitale criteresJson AVendre:false via Firecrawl, Proprio Direct rentOrSale:rent, Ubee Renter, Barnes Algolia type:rental, M Immobilier CODE_STATUT=AL), 265 sources au registre, backends Scrapfly portés dans base.py (SCRAPFLY_KEY ajoutée au .env du node), helper _detailutil (cache fiches détail). Commit dcb5008. | 2026-08-13: connexion déléguée au HUB KA ID (groupe-ka.com) — auth.py: routes /api/auth/ka/login (redirige vers {KA_HUB_URL}/sso/authorize, state signé) + /api/auth/ka/callback (vérifie le JWT HS256 du hub en stdlib, upsert local: match google_sub=\"ka:<hubid>\" puis par courriel — les ex-comptes Google directs gardent leur google_sub —, cookie louka_session, /bienvenue si nouveau), /api/auth/config renvoie {google, ka}, /api/me provider \"ka-id\"; frontend: 6 boutons basculés vers /api/auth/ka/login (« Se connecter avec KA ID », badge KA au header), dist rebuildé. .env node: + KA_SSO_SECRET (partagé avec le hub) + KA_HUB_URL. L'ancien flux Google direct reste en place (fallback). Testé bout-en-bout curl. | 2026-08-13 comptes v2+v3 : SSO « Se connecter avec KA » via hub groupe-ka (adopte le ka_id du hub), rôles locataire/gestionnaire (/bienvenue), favoris (cœur + /favoris), pages gestionnaires réclamables et personnalisables (/gestion + /g/{source}), profil enrichi (photo téléversée Pillow 512px → data/uploads/ — ⚠️ exclure data/uploads des rsync --delete, bio, ville, réseaux sociaux), profil public /u/{ka_id}, carte de membre. python-multipart ajouté au venv du node. | 2026-08-13 v2: PROFIL DICTÉ PAR LE HUB groupe-ka.com — louka/hubprofile.py (fetch signé HMAC vers /api/sso/profile, cache TTL 60s positif+négatif, to_epoch UTC — piège: created_at ISO naïf du hub parsé en heure locale = +4h, corrigé). /api/me: champs hub remplacent les locaux + role_label/job_title/company/age/public_url/profile_source; /api/users/{ka_id}: le hub dicte (404 si privé au hub), fallback local pour comptes legacy. Frontend: carte de membre marquée « Groupe KA » (plus Lou·Ka) + badge statut lime, section « Mon profil Groupe KA » EN LECTURE SEULE + bouton « Modifier sur groupe-ka.com » (l'éditeur local ne reste que pour profile_source=local), profil public = état+lien du hub. Testé bout-en-bout (privé au hub → 404 sur lou-ka après TTL). | 2026-08-13 v3: FAVORIS POUSSÉS AU HUB — louka/hubfav.py (push_favorite en thread non bloquant, signé HMAC; listing_item compose titre/type·secteur/prix/photo/lien /logement/{uid}), accounts.py add/remove_favorite poussent add/remove (comptes ka- seulement, legacy ignorés). Rattrapage one-shot exécuté (0 favori legacy relié). Testé bout-en-bout. 2026-08-13 : SEO — SSR léger (louka/seo.py), pages /villes /ville/{v}[/{type}], robots.txt, sitemaps dynamiques, JSON-LD, 410 annonces retirées, GZip. | 2026-08-13: Ka Maps déployé (frontend Mapbox 3D + backend bbox/index géo + geocode batch au watch) | 2026-08-15: IDENTITÉ VISUELLE OFFICIELLE — palette marine #0B1330 / bleu vif #2258FF / orange #FF6A00 / gris #F2F2F2, logo « porte entrouverte » (Logo.tsx, favicon+PWA marine), wordmark Lou-Ka (Ka orange), baseline « La porte d'entrée vers votre prochain chez-vous. », héro « Trouvez votre prochain chez-vous. » + rangée 4 avantages, Inter partout, pill-shaped + ombres douces (styles.css réécrit), tab bar mobile 4 onglets (active orange), thème Ka Maps marine/orange, immeuble 3D orange, PDF+SSR alignés. Rsync (data/ et .env préservés) + pm2 restart lou-ka-web/lou-ka-sync OK, site vérifié (Playwright). Commits: lou-ka b30336e, lou-ka-ios c9aebaa (Theme.swift) — poussés sur git.spboucher.ai. | 2026-08-17 harmonisation ka-ui : retour à l'éditorial sharp avec ACCENT ORANGE #ff6a00 (identité marine 08-15 remplacée, logo porte conservé), tokens vendorés frontend/src/ka/, GroupeKaBadge, KaFooter, /contact, title « Un service Groupe KA », og.png/favicon, SSO vérifié. NB : ka-maps redéployé sur le nœud (dépendance file:../../ka-maps). | 2026-08-18 CAMPAGNE CONNECTEURS (commits 689cd7d + 7ea14bb) : rivero/trudel désactivés (RealVuu DNS mort, 244 zombies archivés), groupe_dallaire récupéré via Scrapfly ASP (IP du nœud bloquée domaine entier, ~66k crédits/j), disjoncteur sources en échec, capreit/logisquebec/royal_lepage enrichis ; colonnes bedrooms (96,7%)/bathrooms (51,1%) + backfill 45k ; fb_marketplace 43→7,3 min (parallélisé), logisquebec 19→7,6 min ; NOUVEAUX connecteurs kangalou (7 916) + rentals_ca (3 452, ~1 600 nettes) ; visites virtuelles 1 107 ; 275 sources au registre ; docs/connecteurs générées. | VAGUE 3 (303d902) : zumper API interne DIRECT (rituel CSRF, bbox, buildings éclatés — +1 954 nettes ; PadMapper = même API vérifiée), atimco Montérégie (+299, base _livya.py), simplex. apartments.com 403 CoStar documenté. 281 sources, 48 401 visibles. | 2026-08-19 : CAMPAGNE FRONT-END MOBILE (13 sites) — menus déroulants tactiles fiabilisés (verrous de scroll, backdrops portalisés hors headers backdrop-filter, hamburgers ajoutés à job-ka/trouve-ka/auto-ka), éléments du bas stabilisés (dvh, safe-area via viewport-fit=cover, échelle z-index --z-*), anti-zoom iOS ≥16 px, cibles tactiles ≥44 px, overflow-x clip html+body ; socle mobile commun dans ka-ui/tokens.css + ka-agent v2 ; audit+régression : ka-ui/mobile-audit.mjs & regression-mobile.mjs, inventaire+rapport : ka-ui/audit/. Commit poussé par app sur son nœud." | |
| 39 | + }, | |
| 40 | + { | |
| 41 | + "app": "vrai-prix", | |
| 42 | + "node": "M3U96a", | |
| 43 | + "port": 8090, | |
| 44 | + "domain": "www.vrai-prix.com", | |
| 45 | + "process_manager": "pm2", | |
| 46 | + "label_or_name": "vrai-prix + vrai-prix-ngrok", | |
| 47 | + "dir": "~/apps/vrai-prix", | |
| 48 | + "start": "cd ~/apps/vrai-prix && pm2 start ./node_modules/.bin/next --name vrai-prix --interpreter none -- start -H 0.0.0.0 -p 8090 ; pm2 start /opt/homebrew/bin/ngrok --name vrai-prix-ngrok --interpreter none -- http --url=www.vrai-prix.com 8090", | |
| 49 | + "deployed": "2026-08-11", | |
| 50 | + "notes": "Plateforme d'estimation immobilière QC (Next.js 16 App Router, TS strict, Tailwind, bilingue FR/EN, Leaflet). Moteur: modèle hédonique LightGBM pré-calculé (22M estimations 2021-2026, MdAPE 11%) blendé 65/35 avec comparables ajustés (marché/superficie/âge, médiane pondérée), fourchette P10-P90, indice de confiance A-D. SQLite 1.2 Go (data/vraiprix.db): 3 747 008 unités du rôle 2026 + 745 119 transactions + FTS5 adresses. API <20 ms. Tests moteur: npm test (15 pass). Sources laptop: ~/Desktop/qc_house_eval/vrai-prix (app), scripts/ (pipeline données: extract_roles.py, merge_transactions.py, hedonic.py, build_vrai_prix_db.py). Node imposé par l'utilisateur malgré la règle M3U96a=staging. pm2 save fait. En-tête auteur Simon-Pierre Boucher — contact@spboucher.ai partout. | 2026-08-08 v2: refonte design complète — identité « éditorial sharp » portée de Lou-Ka (papier grainé #f5f3ee, encre #141814, lime électrique #d9f26b, Space Grotesk/Inter/JetBrains Mono, bordures encre + ombres décalées, ticker marquee, héros uppercase outline+highlight, stat-chips, tableau éditorial, section lead sur fond encre). Tokens dans globals.css (vp-card, btn, pill-conf-A..D, kicker, klabel, hbar). Rebuild + pm2 restart OK. | 2026-08-08 v3: fiche propriété ultra-détaillée (DB rebuild 1,5 Go: +frontage, lien physique, genre constr., matricule, valeurs terrain/bâtiment/antérieure, cond. marché, unité voisinage — codes décodés via domaines MAMH). Carte maison RadarMap.tsx (SVG from scratch, plan d'arpenteur: anneaux distance, azimut réel, N, échelle, hover sync tableau; Leaflet retiré). Rapport PDF vectoriel /api/report (pdfkit + fonts TTF dans assets/fonts, 2 pages: logo, fiche, hbars historique, radar dessiné, comps, méthodo — ~150 ms). Pages /methodologie (7 sections + tableau IAAO), /conditions, /confidentialite (Loi 25, responsable S-P Boucher contact@spboucher.ai) + CookieConsent (bandeau + panneau sélecteur, localStorage vp-cookie-consent 12 mois, bouton footer). serverExternalPackages: better-sqlite3 + pdfkit. | 2026-08-08 v4: page /parc (Parc immobilier) — multi-adresses (SearchBox mode onPick, liste persistée localStorage vp-parc, max 40), agrégation estimatePortfolio() (totaux, fourchette cumulée, confiance pondérée par valeur, croissance 2021→2026, répartitions mun/type, sommes registre), dashboard tuiles encre/lime + hbars historique + cartes par propriété, PDF multi-propriétés /api/report/portfolio (couverture agrégée + 1 page/propriété, report-portfolio.ts). HeaderNav pilule (Estimer/Parc/Méthodologie). | 2026-08-08 v5: MetricViz.tsx (SVG data-driven: BuildingGlyph façade étages/logements/genre, LotDiagram terrain à l'échelle frontage×profondeur+empreinte, ValueSplit terrain/bâtiment animé, ConfidenceDial jauge, Sparkline 2021-26, EraLine frise, CountUp) — intégré ResultView (section Portrait de la propriété bento + registre brut en <details>, YoY badges historique, animations rise/grow-x) + cartes /parc (sparkline+split). Rapport PRO /api/report/pro (report-pro.ts, 6 pages format évaluateur: page titre, sommaire exécutif, mandat/définition valeur marchande, description, grille d'ajustement 4 comps, indice marché, analyse hédonique, réconciliation, 9 conditions limitatives, attestation — encadrés 'pas un rapport OEAQ'). | 2026-08-08 v5.1: correctifs display métriques — CountUp SSR-safe (rend la valeur finale côté serveur, anime 0→valeur au montage; plus jamais '0 $' au premier paint), nombres localisés via locNum() (1 206,6 m² FR), VacantGlyph (borne d'arpenteur + conifères pour CUBF 9xxx — plus de fausse maison), LotDiagram placeholder condo ('quote-part de copropriété') quand superficie nulle, ValueSplit masque les segments <1% et labels <14%, EraLine s'étire avant 1900, écart vs rôle signé, grille historique auto (mobile). | 2026-08-08 v6: page /stats (nav 'Stats') — chiffre-choc provincial 2 009 452 277 400 $ (somme des 3,75M estimations 2026) en CountUp 2,6s sur carte encre, comparaisons (220k$/habitant, ×12 budgets QC, ×3 PIB), tuiles physiques, valeur provinciale par année avec YoY, répartition par type, top 10 municipalités + table top 200 filtrable. Données précalculées scripts/build_stats.py -> src/data/stats.json (17K, embarqué; re-générer après tout rebuild de la DB). | 2026-08-08 v6.1: menu hamburger mobile (HeaderNav — pilules desktop md+, tiroir plein-largeur animé <md avec backdrop), Stats+Parc ajoutés au FooterNav, rapport statistique provincial PDF /api/report/stats (report-stats.ts, 7 pages: chiffre-choc 2,01 billions sur carte encre, 8 tuiles comparaisons, barres par millésime et par type, palmarès complet 200 municipalités paginé 42/page) + bouton download sur /stats. | v6.2: fix menu mobile — le backdrop-blur du header créait un containing block cassant les descendants position:fixed; remplacé par panneau pleine largeur absolute inset-x-0 top-full sous l'en-tête (bg-paper, bordure encre, animation rise, ● actif), plus de voile fixed. Fix total pages rapport stats (6, pas 7). | v6.3: menu mobile refait en plein écran via createPortal(document.body) — plus AUCUN ancêtre avec backdrop-filter (cause racine des rendus transparents/rognés sur WebKit) : takeover fond papier opaque z-3000, barre marque+✕, liens géants numérotés 01-04 uppercase (actif en encre/lime), bascule de langue et liens légaux au pied, scroll body verrouillé, safe-area iOS. | v6.4: CAUSE RACINE menu mobile trouvée via Playwright (tap intercepté par <main>) — la règle hors-@layer 'body > * { z-index:1 }' de globals.css écrasait TOUTES les utilities z-* (les règles non-layerées battent @layer utilities en cascade Tailwind v4) → header et menu à z=1 sous le contenu. Fix: retrait du z-index de la règle. Menu mobile refait en checkbox+peer-checked (PUR CSS, fonctionne sans JS/hydratation morte/cache périmé), header opaque sur mobile (blur seulement md+). Testé Playwright viewport iPhone avec JS activé ET désactivé: ouverture+navigation OK, fond opaque rgb(245,243,238). playwright en devDependency. | v7: identité ROUGE (accent #ff5148, bordeaux #9e2a25 — vars --lime/--green conservent leurs NOMS, seules les valeurs changent) distincte de Lou-Ka. README détaillé (badges, IAAO, archi). Repo GitHub PUBLIC: https://github.com/spboucher-ai/vrai-prix (data/ exclue). | 2026-08-09 v8: agent IA « Ka » (page /ka + API /api/ka) — Claude Haiku 4.5 en streaming SSE, boucle agentique manuelle avec 11 outils serveur (chercher_propriete FTS, evaluer_propriete, comparables_detailles, indice_marche, stats_municipalite/provinciales, evaluer_parc, comparer_proprietes, estimation_manuelle, chercher_proprietes_secteur, liens_rapports), prompt caching system+tools, clarification si adresse ambiguë. ANTHROPIC_API_KEY dans ~/apps/vrai-prix/.env.local sur le node (jamais versionnée, .env* dans .gitignore). Lien Ka dans header+footer. | v8.1: Ka renforcé — recherche approximative (relaxation progressive FTS), outil dossier_complet (éval+marché+tendance+comps+synthèse en 1 appel), tableaux HTML dans le chat, fil à défilement interne (fix saut de page en streaming), mobile refait, 12 tours/2200 tokens. | 2026-08-11: recherche par CARTE (page /carte Leaflet+OSM, popups éditoriales, géolocalisation, garde de zoom) + API GET /api/nearby?lat&lng&halfLat&halfLng&limit (bbox sur idx_units_geo, plafonds serveur ~2,2 km / 400 unités) + nav Carte FR/EN. Dépendance leaflet ajoutée. Déployé (npm install + build + pm2 restart). Sert aussi l'app iOS native (ios/ dans le repo: SwiftUI 17+, onglet Carte MapKit, bundle ai.spboucher.vraiprix). | 2026-08-13: SSO « Se connecter avec KA ID » via le hub groupe-ka.com — src/lib/ka-auth.ts (node:crypto pur, zéro dépendance: vérif JWT HS256 du hub aud=vrai-prix + session cookie vp_ka_session signée KA_AUTH_SECRET, state embarqué signé sans cookie), routes /api/auth/ka/{login,callback} + /api/auth/{me,logout}, AccountButton dans le header (bilingue), page /compte = carte de membre Groupe KA (gabarit .pc encre+lime, accent rouge conservé ailleurs). Env node .env.local: + KA_SSO_SECRET/KA_HUB_URL/KA_BASE_URL/KA_AUTH_SECRET. Session sans stockage (aucune table users). Testé bout-en-bout curl (ka_id partagé OK). | 2026-08-13: carte de membre UNIFORMISÉE — marque « Groupe KA » sur la carte (plus la marque de l'app), même gabarit que le hub. Déployé+vérifié dans le bundle prod. | 2026-08-13: PROFIL HUB AFFICHÉ — hubprofile.py (fetch signé /api/sso/profile du hub, cache 60s), /api/auth/me enrichi (bio/city/job_title/company/age/website/socials/role_label/public_url/profile_source), page profil: badge statut sur la carte + section « Mon profil Groupe KA » lecture seule + bouton « Modifier sur groupe-ka.com ». Testé bout-en-bout. | 2026-08-13: /carte = Vrai-Prix Maps (Ka Maps, Mapbox 3D, tarball ~/ka-maps/) ; /api/nearby plafonds relevés (additif iOS) | 2026-08-13 SEO programmatique v9: metadataBase+canonical+og+JSON-LD (Organization/WebSite au layout; SingleFamilyResidence/Residence/Place+BreadcrumbList sur les fiches), generateMetadata /estimation/[id] (title=adresse+estimation, description unique), pages clientes scindées en wrapper serveur+*Client.tsx (accueil, methodologie, conditions, confidentialite, parc), pages programmatiques /municipalites + /municipalite/[slug] (1097) + /municipalite/[slug]/[type] (maison|plex|chalet|condo|terrain|maison-mobile|autre, ~5900 pages) — stats agrégées SQLite avec cache mémoire (src/lib/municipalites.ts), échantillon 24-30 fiches liées en HTML, voisines; sitemaps: /sitemap-index.xml (à SOUMETTRE dans Google Search Console) -> /sitemap.xml (7043 pages) + /estimation/sitemap/0-83.xml (45k fiches/fichier, plages rowid, force-dynamic); robots.ts (disallow /api/, /compte); noindex /compte + /estimation/manuelle; section municipalités sur l'accueil + lien footer. Vérifié curl prod (robots, 85 sitemaps, titles uniques, JSON-LD, 404). NB: premier build node avait échoué transitoirement (npm install en cours) — pipeline avec tail masque les exit codes, rebuild OK. | 2026-08-13 v9.1: README ultra-détaillé (10 captures Playwright prod dans docs/screenshots/, régénérables scripts/screenshots.mjs; visite guidée, section SEO, endpoints, env vars). Poussé sur origin git.spboucher.ai (3 commits: SSO, SEO, README). Push github ÉCHOUÉ: compte spboucher-ai SUSPENDU (403) — miroir GitHub à re-pousser une fois le compte rétabli. Redéployé+vérifié. | 2026-08-17 harmonisation ka-ui (tokens en layer(base) — gotcha Tailwind v4 : import brut non-layered écrase les utilities), badge+KaFooter+/contact, GET /api/stats (agrégats publics pour le hub), og/favicon, fix débordement 768 (nav md:→lg:), title suffixé. | 2026-08-18 : docs/PIPELINE-DONNEES.md ajouté au repo (pipeline batch laptop documenté pour continuité). | 2026-08-19 : CAMPAGNE FRONT-END MOBILE (13 sites) — menus déroulants tactiles fiabilisés (verrous de scroll, backdrops portalisés hors headers backdrop-filter, hamburgers ajoutés à job-ka/trouve-ka/auto-ka), éléments du bas stabilisés (dvh, safe-area via viewport-fit=cover, échelle z-index --z-*), anti-zoom iOS ≥16 px, cibles tactiles ≥44 px, overflow-x clip html+body ; socle mobile commun dans ka-ui/tokens.css + ka-agent v2 ; audit+régression : ka-ui/mobile-audit.mjs & regression-mobile.mjs, inventaire+rapport : ka-ui/audit/. Commit poussé par app sur son nœud." | |
| 51 | + }, | |
| 52 | + { | |
| 53 | + "app": "groupe-ka", | |
| 54 | + "node": "M3U96b", | |
| 55 | + "port": 8110, | |
| 56 | + "domain": "www.groupe-ka.com", | |
| 57 | + "process_manager": "pm2", | |
| 58 | + "label_or_name": "groupe-ka + groupe-ka-ngrok", | |
| 59 | + "dir": "~/apps/groupe-ka", | |
| 60 | + "start": "cd ~/apps/groupe-ka && pm2 start ./node_modules/.bin/next --name groupe-ka --interpreter none -- start -H 0.0.0.0 -p 8110 ; pm2 start /opt/homebrew/bin/ngrok --name groupe-ka-ngrok --interpreter none -- http --url=www.groupe-ka.com 8110", | |
| 61 | + "deployed": "2026-08-13", | |
| 62 | + "notes": "Plateforme corporative du holding Groupe KA — présente les 7 agrégateurs automatisés (Lou·Ka, Immo·Ka, Vrai-Prix, ValoPlex, Auto·Ka, Fabri·Ka, Food·Ka) avec leurs couleurs de marque. Next.js. | 2026-08-12 v2 « groupe global · ka = agrégation »: héros repensé (« Mille sites. Un seul ·Ka. », carte-définition dictionnaire ·Ka /ka/ suffixe = agréger, CTA), FIGURE D'AGRÉGATION SVG signature (section #flux: 10 chips de sites sources → faisceau de flèches animées en pointillés + pilule « 450+ connecteurs » → boîte KA « = agrégation » → split en 7 flèches vers les plateformes avec pastilles de marque; sablier ·Ka, scroll horizontal sur mobile, prefers-reduced-motion respecté), allusions ka=agrégateur partout (ticker, titres « Ce que nous agrégeons », CTA « Quelque chose à agréger ? », footer), nav +« L'agrégation », OG description corrigée (disait encore 3 plateformes). GOTCHA: colonnes de grille du héros en minmax(0,…fr) + min-w-0 sinon le rail de stat-chips (nowrap) gonfle le min-content et fait déborder la carte hors conteneur. Source laptop désormais: ~/Desktop/groupe-ka (rsync → node + ./node_modules/.bin/next build + pm2 restart groupe-ka). | 2026-08-13 v3: 5 NOUVELLES FIGURES (src/app/figures.tsx, le sablier y a été déplacé): pipeline vertical 5 stations avec boucle « resync en continu », cycle ·Ka (anneau pointillé animé, 4 gestes autour de KA « jour et nuit »), barres d'inventaire vivant (5 plateformes marketplace, une teinte verte, étiquettes directes + pastilles de marque — règles dataviz), matrice de connecteurs « un point = un connecteur » (317 points: Auto·Ka 124/Lou·Ka 108/Immo·Ka 65/Food·Ka 20), tuiles de couverture Vrai-Prix 3 747 008 / ValoPlex 393 867 + carte encre « l'équation ·Ka » (1000+ sites → 450+ connecteurs → 7 plateformes); nouvelle section #chiffres + nav « Chiffres » (hrefs nav passés en /#… pour marcher depuis les sous-pages). PAGES LÉGALES /conditions + /confidentialite (Loi 25, responsable = admin@groupe-ka.com, CAI mentionnée). COURRIELS @groupe-ka.com placés: contact@ (header, menu mobile, CTA, footer — projets/partenariats), info@ (CTA + footer — médias/questions générales), admin@ (footer + pages légales — légal/vie privée); footer avec bloc « nous joindre » 3 adresses + liens légaux; contact@spboucher.ai retiré de l'UI (reste dans les en-têtes auteur du code). | 2026-08-13 v4 — KA ID (hub d'identité du groupe): comptes courriel/mdp (scrypt) + connexion Google + SSO « Se connecter avec KA » pour toutes les plateformes. Better-sqlite3 (~/apps/groupe-ka/data/ka-id.db — EXCLU du rsync de déploiement, ne jamais l'effacer) + jose. Routes: /api/auth/{register,login,logout,me}, /api/auth/google + /api/auth/callback/google (id_token vérifié via JWKS), /sso/authorize (client_id+redirect_uri whitelistés dans src/lib/sso.ts, jeton HS256 5 min signé du secret partagé par client), pages /connexion + /compte, UserButton header + « Mon KA ID » menu mobile. .env.local sur le node (chmod 600): BASE_URL, KA_AUTH_SECRET, KA_SSO_SECRET_LOU_KA (partagé avec lou-ka), GOOGLE_CLIENT_ID/SECRET = client OAuth DÉDIÉ « Groupe KA » créé par l'utilisateur le 2026-08-13 (380896406983-1ph2…, même projet GCP que lou-ka; origine https://www.groupe-ka.com + redirect https://www.groupe-ka.com/api/auth/callback/google enregistrés). Connexion Google ACTIVE (authorize vérifié 302). Nav desktop passée à lg (débordait entre md et lg avec 6 items + 2 boutons). SSO testé bout-en-bout par curl (register hub → ka/login lou-ka → jeton → session lou-ka, compte test supprimé des 2 DB). LOU-KA MODIFIÉ EN MÊME TEMPS (voir entrée lou-ka). | 2026-08-13 KA ID v1 : colonne users.ka_id (unique, « ka- »+10 chiffres) créée AU HUB (source de vérité du groupe) — attribution à l'inscription courriel + Google, rattrapage auto à l'ouverture de la DB, claim ka_id dans le jeton SSO /sso/authorize, affichage sur /compte. SECRETS dans ~/apps/groupe-ka/.env.local sur le node (KA_SSO_SECRET_LOU_KA = KA_SSO_SECRET de lou-ka) — ⚠️ TOUJOURS exclure .env* et data/ des rsync --delete. DB hub: ~/apps/groupe-ka/data/ka-id.db. | 2026-08-13 v5: KA-ID minté PAR LE HUB (colonne users.ka_id + ensureKaId, transporté dans le jeton SSO — les plateformes l'adoptent), last_login, /compte refait = CARTE DE MEMBRE Groupe KA (gabarit .pc de Lou·Ka porté en .mc dans globals.css: fond encre, filigrane KA, KA-ID mono lime glow, bande code-barres) + grille d'infos + copier KA-ID. Secrets SSO générés pour les 7 clients (KA_SSO_SECRET_{LOU_KA,IMMO_KA,VRAI_PRIX,VALOPLEX,AUTO_KA,FABRI_KA,FOOD_KA}) et distribués aux nœuds des apps. ⚠️ INCIDENT 2026-08-13: ~/apps/groupe-ka/.env.local a été écrasé (probable déploiement parallèle avec --delete) → Google/KA_AUTH_SECRET perdus, reconstruit; BACKUP désormais dans ~/.groupe-ka.env.backup sur M3U96b (chmod 600) — le restaurer si ça se reproduit; TOUJOURS exclure .env* ET data/ des rsync de cette app. | 2026-08-13 v6: CODE QR sur la carte de membre /compte (lib qrcode, data-URL PNG, cadre lime) pointant vers la page publique /m/{ka_id} qui transmet le profil (nom, avatar, ancienneté, badge « authentique et actif » — jamais le courriel; ID invalide → introuvable; noindex). Apple Wallet (.pkpass) faisable ensuite: nécessite un Pass Type ID + certificat du compte développeur Apple de l'utilisateur (pas encore créé). NOTE RÉSEAU: si SSH Tailscale vers M3U96b timeout alors que le site répond, déployer via rebond LAN: rsync -e 'ssh -A -J M3U96a' vers simon-pierreboucher@192.168.2.82 (IP DHCP, re-vérifier). | 2026-08-13 v7: APPLE WALLET ACTIF (cert Pass Type ID pass.com.groupe-ka.membre installé 2026-08-13, expire 2027-09-12 — RENOUVELER avant via developer.apple.com avec le CSR/clé de ~/Desktop/groupe-ka-wallet/) — /api/wallet/pass génère un .pkpass signé (pass.json générique encre/lime avec KA-ID + QR /m/{ka_id} + backfields, icônes assets/wallet/ générées PIL, manifest SHA-1, signature PKCS#7 via openssl smime, zip) ; bouton « Ajouter à Apple Wallet » sur /compte rendu SEULEMENT si env KA_PASS_DIR+KA_PASS_TYPE_ID présents. Sur le node: ~/apps/groupe-ka/wallet/ contient déjà pass-key.pem + wwdr.pem (chmod 600). RESTE À FAIRE (utilisateur): créer Pass Type ID (ex. pass.com.groupe-ka.membre) + certificat sur developer.apple.com avec le CSR ~/Desktop/groupe-ka-wallet/Groupe-KA-Pass.certSigningRequest (clé privée locale ~/Desktop/groupe-ka-wallet/pass-key.pem, Team ID 3YM54G49SN) ; puis convertir pass.cer→pass-cert.pem, scp vers wallet/, ajouter au .env.local KA_PASS_DIR=/Users/simon-pierreboucher/apps/groupe-ka/wallet + KA_PASS_TYPE_ID + KA_PASS_TEAM_ID=3YM54G49SN, pm2 restart. | Wallet vérifié en prod: /api/wallet/pass → 200 application/vnd.apple.pkpass, signature PKCS#7 valide (chaîne WWDR G4 + Pass Type ID), QR = /m/{ka_id}, bouton visible sur /compte. | 2026-08-13 v8: PHOTO DE PROFIL — upload sur /compte (POST/DELETE /api/profile/avatar, multipart ≤8 Mo, sharp rotate+cover 512×512 JPEG q88, stockée data/avatars/{ka_id}.jpg — data/ exclue des rsync), servie publiquement par GET /avatars/{ka_id} (cache 1h), avatar_url = URL absolue+cache-bust → voyage dans le jeton SSO (toutes les plateformes l'affichent à la connexion suivante). Carte Wallet: vignette thumbnail(@2x/@3x).png générée de la photo. Page QR /m/{ka_id} refaite en PAGE D'IDENTIFICATION: grande photo ronde 176px (ou initiale), nom en gros, badge « Membre vérifié », carte KA-ID encre/lime, grille d'infos (identifiant, membre depuis, dernière connexion, type de compte, valide sur les 7 plateformes) — jamais le courriel. Testé bout-en-bout (upload+recadrage+page+pkpass avec thumbnails, compte test nettoyé). | 2026-08-13 v9: TYPES DE COMPTE obligatoires à l'inscription (src/lib/roles.ts: membre « Utilisateur des plateformes » / fournisseur « Fournisseur · partenaire » / equipe « Équipe Groupe KA ») — sélecteur cartes radio dans AuthForm (register 400 sans rôle), comptes Google choisissent via RolePicker sur /compte (bandeau tant que role NULL), POST /api/profile/role. AFFICHÉ SUR LA CARTE: badge lime rotaté .mc-holder-role sous le nom, ligne « Type de compte » dans la grille, « Statut » sur la page /m/{ka_id}, champ STATUT sur le pass Apple Wallet, claim ka_role dans le jeton SSO. Compte de l'utilisateur spbou4@protonmail.com (ka-4693099255) marqué role=equipe en base à sa demande. Testé bout-en-bout. | 2026-08-13 v10: PROFIL ENRICHI = SOURCE DE VÉRITÉ DU GROUPE — colonnes users bio/city/phone/website/job_title/company/birth_date/socials(JSON 6 réseaux)/public; éditeur complet sur /compte (ProfileForm: nom affiché, bio 280, ville, emploi, entreprise, date de naissance→âge calculé, téléphone, site, 6 réseaux; POST /api/profile) + toggle PROFIL PUBLIC (POST /api/profile/public) avec lien groupe-ka.com/u/{ka_id}; page publique /u/{ka_id} (opt-in, grande photo, badge statut, bio, emploi·entreprise, ville, âge, chips site+réseaux, lien vérif /m/, privé=introuvable indistinct, JAMAIS courriel/téléphone); API SERVEUR-À-SERVEUR pour les plateformes GET /api/sso/profile?client_id&ka_id&ts&sig (HMAC-SHA256 du secret SSO du client sur `client.ka_id.ts`, fenêtre ±5 min) → profil complet JSON (incl. email/phone — plateformes de confiance). Les plateformes doivent afficher CE profil sur leurs pages utilisateur (lou-ka corrigé le premier). Testé bout-en-bout. | 2026-08-13 v11: CARTE PHYSIQUE & NFC — section sur /compte (lien https://www.groupe-ka.com/m/{ka_id} à écrire sur puce NTAG215 + bouton copier + mode d'emploi NFC Tools en 5 étapes) et page /compte/carte = carte IMPRIMABLE format CR80 exact (85,6×54 mm, rayon 3,18 mm, repères de découpe pointillés, print-color-adjust, @media print masque header/ticker/footer/grain): recto encre (marque, KA-ID lime, nom, badge statut, membre depuis, photo) + verso blanc (QR 30 mm, « Tapez la carte (NFC) ou scannez », zone pointillée où coller la puce, liste des 7 plateformes). | 2026-08-13 v12: FAVORIS UNIFIÉS « MON UNIVERS KA » — le hub est LE magasin central: table favorites (user_id+app+item_id uniques, title/subtitle/price_label/image_url/url/meta), API serveur-à-serveur /api/sso/favorites (POST add/remove + GET ?scope=app|all, HMAC verifyClientSig factorisé dans src/lib/sso.ts), API session /api/favorites (GET + POST remove), section « Mon univers Ka » sur /compte (groupée par app avec pastilles de marque, vignettes, prix, lien profond, bouton retirer, état vide pédagogique). Testé bout-en-bout: ♥ posé sur lou-ka → visible sur /compte du hub → retiré → disparu. | 2026-08-13 : Sign in with Apple ajouté (Services ID com.groupe-ka.web, Team 3YM54G49SN, Key 2L3VSACA87 ; APPLE_* dans .env.local, clé .p8 aussi sur le laptop ~/Desktop/AuthKey_2L3VSACA87.p8). Colonne apple_sub en DB. NB: SSH M3U96b depuis le laptop = ProxyJump via M3U96a (192.168.2.82) — port 22 Tailscale/LAN direct bloqué, voir ~/.ssh/config. | 2026-08-13 v2: boutons OAuth officiels (G coloré/pomme noire), icône compte dans le header mobile (retirée du menu déroulant), README avec badges+captures. Source versionnée: https://git.spboucher.ai/groupe-ka (spbgit). | 2026-08-13 v13: SECTION KA2 (#ka2, entre socle et manifeste) — l'agent de découverte web du groupe (www.ka2.bot, bot IA de cartographie du web québécois, Claude Haiku 4.5 + Firecrawl/Scrapfly, robots.txt/Loi 25): carte produit wordmark Ka[2] + carte encre « Ka2 découvre → ·Ka agrège → vous trouvez »; nav « Ka2, le bot », ligne ticker, lien footer. Commit 62bfdb2. | 2026-08-13 v14: KA4 AJOUTÉ — la section devient #bots (« les bots découvrent »): cartes jumelles Ka[2] (lime) + Ka[4] (orange #ff9f45, 5 types d'entités, tableau de bord live) via tableau BOTS, carte encre commune pleine largeur « Les bots découvrent → ·Ka agrège »; nav « Les bots » (/#bots), ticker « Ka2 & Ka4 », footer +Ka4 (www.ka4.bot). Commit a966e8c. | 2026-08-13 v15: PAGE /loi-25 « Protection des renseignements personnels (Loi 25) » — politique complète (15 cartes: responsable = Simon-Pierre Boucher / admin@groupe-ka.com, nature d'agrégateur de contenu public, tableau des 7 apps et données traitées, note estimations automatisées Vrai-Prix/ValoPlex, témoins essentiels seulement — pas de bandeau requis, hébergement Canada, droits + CAI, retrait 48-72h ouvrables via admin@, incidents, MAJ 13 août 2026); lien footer + lien croisé depuis /confidentialite. Commit 7959e3b. | 2026-08-13 v16: PAGE /bots « Déclaration de transparence — robots d'indexation » — 8 principes communs (données publiques, robots.txt, identification, charge minimale, extraits seulement, journalisation, retrait 48-72h via admin@, Loi 25/LPRPDE), fiches ka·2 (cartographie générale, 4 types), ka·4 (élargie, 5 types + live) et ka·6 (NOUVEAU bot — créateurs/influenceurs QC, www.ka6.bot, droit de retrait renforcé), « ce que nos robots ne font pas », section exploitants de sites (fréquence/exclusion/partenariat contact@); lien footer « Transparence des robots d'indexation », Ka6 ajouté aux liens footer, lien croisé /loi-25 ↔ /bots. NB: page /bots ≠ ancre /#bots de l'accueil. Commit 98e410b. | 2026-08-13 v17: PAGES /retrait + /avis. /retrait « Politique de retrait et de révision » — Partie A retrait d'annonces (qui peut demander, 6 motifs, procédure, table des preuves minimales src-table, avertissement anti-pièce-d'identité encadré pointillé, liste d'exclusion), Partie B Vrai-Prix/ValoPlex (B1 révision de données, B2 arrêt de couverture avec preuve de propriété + liste d'exclusion permanente, B3 copropriétés), traitement/refus/recours CAI (retrait 48-72h, révisions ≤10 j ouvrables); tout via admin@groupe-ka.com. /avis « Notre rôle d'agrégateur » — clause « AUCUN CONTRAT » en encadré encre/lime en tête, puis « en conséquence » (ni vendeur/courtier, sources tierces à vérifier, estimations indicatives, aucune commission). FOOTER: ligne d'avis en évidence (bordure lime « nous ne vendons rien… » + En savoir plus → /avis) + liens légaux /retrait et /avis; liens croisés /loi-25→/retrait et /bots→/retrait. Commit 72e8cd1. | 2026-08-13 v18: TROUVE·KA PRODUIT PHARE — section #trouve-ka juste après le héros (carte moteur: wordmark Trouve[Ka] bleu #b9cfee, barre de recherche factice, chips, 4 features, CTA; carte encre « Les grands moteurs cherchent le monde. Trouve·Ka cherche le Québec. »); héros « sept agrégateurs et un moteur de recherche » + chip; rangée socle (8 plateformes); nav +« Moteur » — BREAKPOINT NAV lg→xl + libellés raccourcis (Socle/Bots) + whitespace-nowrap + px-3 (8 items débordaient à 1024-1280); ticker; footer Trouve·Ka en tête de liste; meta description; ajouté à /avis, /loi-25 (rangée tableau moteur de recherche) et /retrait. Trouve-ka live: M2M32:3000, ~57k pages/7.9k domaines au moment de l'ajout (croissance ~36k pages/h — chiffres volontairement non figés sur le site). Commit 7378bce. | 2026-08-17 : ajout des 3 nouveaux agrégateurs Resto·Ka, Sorti·Ka et Créa·Ka (cartes 08-09-10 avec badge Nouveau, données réelles des APIs), refonte des cartes produits (bande d'accent de marque, numéro en pastille encre/accent, étiquette de marché, puces couleur marque, 10e carte pleine largeur), figures à 10 plateformes/13 sources/340 connecteurs, socle 11 plateformes, ticker+footer+OG à jour. Rebuild + pm2 restart OK. Commit 2389176 pushé sur git.spboucher.ai/groupe-ka. | 2026-08-17: client SSO crea-ka ajouté au registre KA ID (sso.ts + KA_SSO_SECRET_CREA_KA en env), rebuild + pm2 restart OK (commit a656d95). | 2026-08-17 : .env RESTAURÉ (7 secrets KA_SSO_SECRET_* — était absent, SSO KA ID cassé pour toutes les plateformes) + client sorti-ka inscrit au registre SSO (src/lib/sso.ts), rebuild + restart. | 2026-08-17 (2) : PDF « L'agrégation, expliquée » (12 pages A4 paysage, schémas sablier/pipeline/cycle/bots/Trouve·Ka/inventaire/flotte/moteurs/socle/manifeste) — source assets/poster/groupe-ka-agregation.html (figures JS, rendu via chrome-headless-shell de playwright ~/Library/Caches/ms-playwright), servi sur /documents/groupe-ka-agregation.pdf (public/), bouton de téléchargement dans #flux. | 2026-08-17 HARMONISATION ÉCOSYSTÈME : hub du design system ka-ui (repo spbgit ka-ui — tokens éditorial sharp + accents 12 marques + ecosystem.json centralisé + og/favicons). SSO RÉPARÉ (le .env.local avait été écrasé → restauré de ~/.groupe-ka.env.backup, secrets par client resynchronisés depuis les satellites, backup re-synchronisé) ; clients api-ka + trouve-ka ajoutés (14 clients). Métriques LIVE (src/lib/live.ts ← /api/stats des plateformes, ISR 10 min, replis) : ticker, héros, cartes, figures, socle. Page /status v2 avec HISTORIQUE (pm2 cron groupe-ka-status-tick 5 min → /api/status/tick → data/status/history.jsonl 90 j ; barres 24 h + 90 j, uptime 24h/7j/30j). API·Ka au footer. og.png/favicon.svg servis. | 2026-08-17 (2) : tableau de bord MAÎTRE /stats (ecostats.ts — consolidation live des dashboards satellites, classement, blocs par plateforme, chips de période) + proxy /api/stats/ecosystem-report (streaming vers api-ka). Job·Ka intégré partout : client SSO (14→15 clients), métriques live (live.ts jobka), ticker, carte plateforme 11, socle, figures (accents officiels corrigés), footer, comptes status/stats dynamiques (13 plateformes). | 2026-08-18 (commit 71c6515) : hub exploite les champs riches des /api/stats (loyer/prix/salaire moyens, géo, santé des syncs, bloc Immo×Vrai-Prix écart +9%, trouve-ka live) ; hardcodes corrigés (SORTIKA_CONNECTORS, ticker plex daté). MANQUE .env : APPLE_* (Sign-in Apple mort) + KA_SSO_SECRET_TOIT_KA. | 2026-08-19 : CAMPAGNE FRONT-END MOBILE (13 sites) — menus déroulants tactiles fiabilisés (verrous de scroll, backdrops portalisés hors headers backdrop-filter, hamburgers ajoutés à job-ka/trouve-ka/auto-ka), éléments du bas stabilisés (dvh, safe-area via viewport-fit=cover, échelle z-index --z-*), anti-zoom iOS ≥16 px, cibles tactiles ≥44 px, overflow-x clip html+body ; socle mobile commun dans ka-ui/tokens.css + ka-agent v2 ; audit+régression : ka-ui/mobile-audit.mjs & regression-mobile.mjs, inventaire+rapport : ka-ui/audit/. Commit poussé par app sur son nœud." | |
| 63 | + }, | |
| 64 | + { | |
| 65 | + "app": "spbgit", | |
| 66 | + "node": "M3U96a", | |
| 67 | + "port": 7420, | |
| 68 | + "domain": "git.spboucher.ai", | |
| 69 | + "process_manager": "pm2", | |
| 70 | + "label_or_name": "spbgit-server + spbgit-tunnel", | |
| 71 | + "dir": "~/srv/spbgit/app", | |
| 72 | + "start": "cd ~/srv/spbgit/app && SPBGIT_LOG_DIR=$HOME/srv/spbgit/logs SPBGIT_NGROK_CONFIG=$HOME/srv/spbgit/ngrok.resolved.yml pm2 start deploy/ecosystem.config.cjs --update-env", | |
| 73 | + "deployed": "2026-08-09", | |
| 74 | + "notes": "SPB Git — plateforme git personnelle complète (forge auto-hébergée, équivalent GitHub perso). Fastify 5 :7420 (loopback), Smart HTTP v2 streamé (clone public, push par PAT argon2id), UI SSR Nunjucks (README GitHub-grade badges/mermaid/shiki, heatmap, blame, diffs, OG cards, feed.atom, sitemap), API /api/v1, CLI spbgit (init/create/clone/status/sync/doctor…). Données: ~/srv/git (repos bare = source de vérité), ~/srv/spbgit/{data,cache,logs,backups}. Node IMPOSÉ par le CLAUDE.md du projet (~/Desktop/git) malgré la règle M3U96a=staging. pm2 save fait; tunnel ngrok en exp_backoff (self-heal). ⚠️ BLOQUANT UNIQUE: réserver git.spboucher.ai dans dashboard.ngrok.com/domains (le CNAME GoDaddy existe déjà → ngrok-cname). Backup nightly cron 03:15 deploy/backup.sh (garde 14). Token bootstrap remis à l'utilisateur (jamais stocké en clair — argon2 dans data/tokens.json). Source laptop: ~/Desktop/git (repo git local, commits conventionnels). En-tête auteur Simon-Pierre Boucher — contact@spboucher.ai sur chaque fichier." | |
| 75 | + }, | |
| 76 | + { | |
| 77 | + "app": "immo-ka", | |
| 78 | + "node": "M4M64a", | |
| 79 | + "port": 8096, | |
| 80 | + "domain": "www.immo-ka.com", | |
| 81 | + "process_manager": "pm2", | |
| 82 | + "label_or_name": "immo-ka-web + immo-ka-sync + immo-ka-ngrok", | |
| 83 | + "dir": "~/apps/immo-ka", | |
| 84 | + "start": "pm2 start .venv/bin/python --name immo-ka-web --interpreter none --cwd ~/apps/immo-ka -- run.py serve 8096 ; pm2 start .venv/bin/python --name immo-ka-sync --interpreter none --cwd ~/apps/immo-ka -- run.py watch 60 ; pm2 start /opt/homebrew/bin/ngrok --name immo-ka-ngrok --interpreter none -- http --url=www.immo-ka.com 8096", | |
| 85 | + "deployed": "2026-08-12", | |
| 86 | + "notes": "Agregateur proprietes A VENDRE au Quebec. Frontend React18+Vite+TS+MapLibre (qualite Lou-Ka): Home filtres+grille+carte, fiche par propriete (galerie+lightbox, specs, tableau caracteristiques Centris, tableau pieces+dimensions, inclusions, historique prix, courtier, mini-carte), Stats, Agences. Backend FastAPI+SQLite, dedup par no Centris (web.py). 65 CONNECTEURS. 13 agences actives: RE/MAX central (~25k, Meilisearch+sharding FSA+detail enrichi), Proprio Direct (~2.4k), Via Capitale (~5.3k Firecrawl), Sutton, Barnes, M Immobilier, KW Urbain, Vendirect, Sotheby's, Profusion (357), Engel&Volkers (1240), KW Distinction (167), L'Expert PM (expertimmobilierpm.com) + 52 sous-agences RE/MAX backup (remax_ag_*, dedup no Centris). Caracteristiques detaillees (chauffage/toiture/annee/stationnement...) captees pour RE/MAX + kw_urbain/proprio/barnes/m/via_capitale/sutton. A_FAIRE (verrouilles): royal_lepage (ESRI da_id), century21 (moxiworks WAF 503), exp_quebec (BoldTrail XHR). Watch 180min. Source ~/Desktop/agent-courtage. | 2026-08-13: connexion « Se connecter avec KA ID » via le hub groupe-ka.com — immoka/auth.py: routes /api/auth/ka/login + /api/auth/ka/callback (JWT HS256 stdlib, aud=immo-ka, users keyed par ka_id du groupe), /me expose enabled avec KA, menu compte frontend affiche le KA-ID. .env node: + KA_SSO_SECRET/KA_HUB_URL/IMMOKA_BASE_URL/AUTH_SECRET. PIÈGE RÉGLÉ: le web.py du nœud était antérieur au laptop (pas d'include_router(auth)) → /api/auth/* tombait dans le catch-all SPA (renvoyait le HTML). Package immoka/ resynchronisé en entier. Testé bout-en-bout curl (ka_id partagé OK). | 2026-08-13 v2: bouton compte sorti de .nav (masquée <760px → invisible sur téléphone) vers .auth-slot toujours visible; pilule NOIRE « KA ID » avec badge KA lime (plus de hover transparent), règles mobiles dédiées. Vérifié par screenshot mobile prod. | 2026-08-13 v3: PAGE PROFIL CRÉÉE (/profil, route + menus) — carte de membre GROUPE KA complète (gabarit .pc, encre/lime identité groupe, badge statut, copier KA-ID, bande code-barres), section « Mon profil Groupe KA » lecture seule (bio/emploi·entreprise/ville/âge/site/réseaux avec 6 icônes maison) + « Modifier sur groupe-ka.com »; immoka/hubprofile.py + /api/auth/me enrichi (profile_source, ancienneté locale+hub). Testé bout-en-bout. | 2026-08-13 v4: FAVORIS ♥ (magasin central = hub, zéro stockage local) — immoka/hubfav.py + immoka/favorites.py (GET /api/favorites {ids,items}, POST /toggle; 401 sans session ou compte non ka-), ♥ sur cartes (grille) + galerie fiche, contexte AccountProvider (toggle optimiste), menu « Mes favoris — Mon univers Ka ». Testé bout-en-bout. | 2026-08-13: VRAIPRIX_DB branché (.env → data/vraiprix.db, 1,75 Go copié de M3U96a) ; backend Ka Maps déployé (geojson bbox/limit/totaux, index géo, watch = geocode batch + vraiprix_local/cycle) ; 59k+ estimations en prod | frontend Ka Maps (Mapbox 3D) déployé | 2026-08-13 SEO: rendu HTML côté serveur (immoka/seo.py) — le catch-all sert du HTML complet par route (title/meta/canonical/og uniques, JSON-LD RealEstateListing+Offer+BreadcrumbList, contenu+liens dans #root que React remplace). Pages programmatiques /a-vendre/{ville}[/{type}] et /type/{type} (seuil 3 annonces, stats médiane/moyenne, pagination 48, maillage interne), fiches /propriete/{uid}/{slug} (301 depuis l'uid nu, 410 Gone si active=0, 404 réel), robots.txt, sitemap index + fiches-N/villes/villes-types/types/pages.xml (lastmod, cache 1 h). Frontend: routes SPA miroirs (pages/Category.tsx, resolveSeo /api/seo/resolve), slugs partagés Python/TS (slugify identique), document.title dynamique. Vérifié curl prod: accueil 57 913, ville/type OK, 301/410/404 OK, sitemaps 57 913 fiches + 1 589 villes + 2 702 villes-types. | 2026-08-17 harmonisation ka-ui (accent cerise #e23744, badge, KaFooter, /contact, /profil branché — AccountProvider n'était monté nulle part, api.ts complété), og/favicon, title suffixé. NB kamaps/ + Category.tsx exclus du tsc (tarball ka-maps absent). | 2026-08-18 CAMPAGNE CONNECTEURS (commits 7911d58 + e9bc9d7 + 7c5bc1b) : barnes 403 → clé Algolia auto-extraite ; CAUSE RACINE parseur remax (tableau Libellé|Valeur inversé, details historiques = bruit) → réécrit label-first + taxes/éval (cache v4, re-parse 1 cycle) ; via_capitale détail (Firecrawl budget 200/c) ; kijiji attrs étendus ; JOINTURE RÔLE vraiprix.db : year_built 20→67%, lot_sqft 17→62%, valeurs officielles sur 55k fiches ; sutton/RLP/lespac fiches (taxes, frais condo, revenus plex, galeries) ; days_on_market ; dédup adresse +1 748 ; registre 95→92 ; docs générées. | VAGUE 3 (feaefa8) : Realtor.ca DÉBLOQUÉ (Scrapfly ASP rejoue PropertySearch_Post, 2 145 fiches, dédup MLS auto → 343 uniques), Sotheby's 60→869 (ajax_results.php, piège p_status), +7 agences source.immo dont ciemcq/mon-toit.net 3 920 fiches, Guide Habitation 95 projets neufs. 100 sources, 145 727 fiches. | 2026-08-19 : CAMPAGNE FRONT-END MOBILE (13 sites) — menus déroulants tactiles fiabilisés (verrous de scroll, backdrops portalisés hors headers backdrop-filter, hamburgers ajoutés à job-ka/trouve-ka/auto-ka), éléments du bas stabilisés (dvh, safe-area via viewport-fit=cover, échelle z-index --z-*), anti-zoom iOS ≥16 px, cibles tactiles ≥44 px, overflow-x clip html+body ; socle mobile commun dans ka-ui/tokens.css + ka-agent v2 ; audit+régression : ka-ui/mobile-audit.mjs & regression-mobile.mjs, inventaire+rapport : ka-ui/audit/. Commit poussé par app sur son nœud." | |
| 87 | + }, | |
| 88 | + { | |
| 89 | + "app": "auto-ka", | |
| 90 | + "node": "M4M64b", | |
| 91 | + "port": 8095, | |
| 92 | + "domain": "www.auto-ka.com", | |
| 93 | + "process_manager": "pm2", | |
| 94 | + "label_or_name": "auto-ka-web / auto-ka-sync / auto-ka-ngrok", | |
| 95 | + "dir": "~/auto-ka", | |
| 96 | + "start": ".venv/bin/python run.py serve 8095 | watch 120 | ~/bin/ngrok http --url=www.auto-ka.com 8095", | |
| 97 | + "deployed": "2026-08-12", | |
| 98 | + "notes": "Agrégateur voitures usagées QC — 35 connecteurs concessionnaires (D2C Media, SM360, AMVOQ, Convertus, EvalAuto, customs), FastAPI + React, SQLite. Firecrawl pour 3 sites Cloudflare (clé dans .env). IMPORTANT: utiliser ~/bin/ngrok (le ngrok homebrew gèle sur ce node). | 2026-08-13: SSO « Se connecter avec KA ID » via le hub groupe-ka.com — autoka/auth.py (modèle immoka: JWT HS256 stdlib aud=auto-ka, users keyed ka_id, cookie autoka_session signé SESSION_SECRET), include_router avant le catch-all SPA, AccountMenu header (pastille KA) + page /profil carte de membre Groupe KA. Env node .env: + KA_SSO_SECRET/KA_HUB_URL/AUTOKA_BASE_URL/SESSION_SECRET. Healthcheck local via python urllib (curl 127.0.0.1 se fige sur M4M64b). Testé bout-en-bout curl (ka_id partagé OK). | 2026-08-13: carte de membre UNIFORMISÉE — marque « Groupe KA » sur la carte (plus la marque de l'app), même gabarit que le hub. Déployé+vérifié dans le bundle prod. | 2026-08-13: PROFIL HUB AFFICHÉ — hubprofile.py (fetch signé /api/sso/profile du hub, cache 60s), /api/auth/me enrichi (bio/city/job_title/company/age/website/socials/role_label/public_url/profile_source), page profil: badge statut sur la carte + section « Mon profil Groupe KA » lecture seule + bouton « Modifier sur groupe-ka.com ». Testé bout-en-bout. | 2026-08-13 v2: FAVORIS ♥ hub — autoka/hubfav.py + fav_router (/api/favorites + /toggle), ♥ sur VehicleCard + fiche (galerie), account.tsx contexte, vehicleFavItem (année marque modèle · concessionnaire·région, /vehicule/{uid}), menu « Mes favoris ». Testé bout-en-bout. | 2026-08-13 SEO: rendu HTML côté serveur (autoka/seo.py) — meta uniques (title/description/canonical/og/hreflang fr-CA), JSON-LD Car+Offer+ItemList+BreadcrumbList, contenu complet dans #root que React remplace. Pages programmatiques /usagees/{marque}[/{modele}], /region/{r}, /ville/{v}, /carrosserie/{c} (liste blanche 10 carrosseries propres), /motos|/scooters/{marque} (~900 pages). Fiches /vehicule/{uid}/{slug} (301 depuis uid nu, slug année-marque-modèle partagé Python/TS, sans trim), 410 Gone si active=0, 404 réels (catch-all). robots.txt + sitemap.xml dynamique (index + pages.xml + vehicules-N.xml par 10k, lastmod). GZipMiddleware. Frontend: SeoListing.tsx lit window.__SEO_CTX__ injecté par le serveur, routes SPA miroirs, document.title dynamique. Vérifié curl prod (accueil 17 082 véhicules en HTML, 301/404/410 OK). RESTE (utilisateur): soumettre le sitemap dans Google Search Console. | 2026-08-17 harmonisation ka-ui (accent #ff5a2a, badge, KaFooter, /contact), og/favicon (+SSR), title suffixé. Lenteur home = images CDN concessionnaires (2 Mo, TTFB 2-3 s) — documenté, pas de fix simple. | 2026-08-18 CAMPAGNE CONNECTEURS (commits 2a35ac2 + bbc070b) : Carfax cascade (D2C badge HTML), fuelConsumption/structuralDamage/listed_at AED, specs moto HTML (fuel/rouage/carrosserie 0→100%), fix magnetis @graph (VIN/photos 0→100%), GPS lat/lng + villes_gps 216 villes (97% actifs) ; DÉDUP VIN (2 315 masqués) ; NOUVEAU kijiji particuliers (+3 002, GPS exact ; kijijiautos.ca n'existe plus) ; rappels Transports Canada (16 969, /api/vehicles/{uid}/recalls) ; table dealers 138/138 ; 139 sources ; docs générées. | VAGUE 3 (4a7b276) : +26 164 annonces — otogo API JSON publique INVENTAIRE COMPLET (23 700, VIN 99%, GPS exact, 0 crédit), autotrader/AutoHebdo direct (1 960, 0 crédit), cargurus deal ratings (ASP 1 crédit/req). Dédup VIN 8 438 masqués. 37 770 visibles, 142 sources. | 2026-08-19 : CAMPAGNE FRONT-END MOBILE (13 sites) — menus déroulants tactiles fiabilisés (verrous de scroll, backdrops portalisés hors headers backdrop-filter, hamburgers ajoutés à job-ka/trouve-ka/auto-ka), éléments du bas stabilisés (dvh, safe-area via viewport-fit=cover, échelle z-index --z-*), anti-zoom iOS ≥16 px, cibles tactiles ≥44 px, overflow-x clip html+body ; socle mobile commun dans ka-ui/tokens.css + ka-agent v2 ; audit+régression : ka-ui/mobile-audit.mjs & regression-mobile.mjs, inventaire+rapport : ka-ui/audit/. Commit poussé par app sur son nœud." | |
| 99 | + }, | |
| 100 | + { | |
| 101 | + "app": "fabri-ka", | |
| 102 | + "node": "M4M64a", | |
| 103 | + "port": 8097, | |
| 104 | + "domain": "www.fabri-ka.com", | |
| 105 | + "process_manager": "pm2", | |
| 106 | + "label_or_name": "fabri-ka-web / fabri-ka-sync / fabri-ka-ngrok", | |
| 107 | + "dir": "~/fabri-ka", | |
| 108 | + "start": ".venv/bin/python run.py serve 8097 | watch 360 | ngrok http --url=www.fabri-ka.com 8097", | |
| 109 | + "deployed": "2026-08-12", | |
| 110 | + "notes": "Agrégateur de produits québécois (marketplace de découverte). Source: ~/Desktop/qc-store. Connecteurs Shopify/WooCommerce/Squarespace génériques pilotés par data/stores.json (1354 boutiques QC, 284 connectables). Resync auto aux 6h. | 2026-08-13: SSO « Se connecter avec KA ID » via le hub groupe-ka.com — fabrika/auth.py (JWT HS256 stdlib aud=fabri-ka, users keyed ka_id, cookie fabrika_session signé SESSION_SECRET), AccountMenu header + page /profil carte de membre Groupe KA (tokens --ka-ink/--ka-lime ajoutés, --font-mono système). Env node .env: + KA_SSO_SECRET/KA_HUB_URL/FABRIKA_BASE_URL/SESSION_SECRET. Testé bout-en-bout curl (ka_id partagé OK). | 2026-08-13: carte de membre UNIFORMISÉE — marque « Groupe KA » sur la carte (plus la marque de l'app), même gabarit que le hub. Déployé+vérifié dans le bundle prod. | 2026-08-13: PROFIL HUB AFFICHÉ — hubprofile.py (fetch signé /api/sso/profile du hub, cache 60s), /api/auth/me enrichi (bio/city/job_title/company/age/website/socials/role_label/public_url/profile_source), page profil: badge statut sur la carte + section « Mon profil Groupe KA » lecture seule + bouton « Modifier sur groupe-ka.com ». Testé bout-en-bout. | 2026-08-13 v2: FAVORIS ♥ hub — fabrika/hubfav.py (router /api/favorites + /toggle inclus dans web.py), ♥ overlay sur ProductCard (Home/Catalog/similaires) + fiche produit, FavoritesProvider optimiste, productFavItem (/produits/{uid}), menu « Mes favoris ». Testé bout-en-bout. | 2026-08-17 harmonisation ka-ui (accent terracotta, badge, KaFooter, /contact) + SSO RÉPARÉ (KA_SSO_SECRET manquait du .env → ajouté ; FABRIKA_BASE_URL défaut localhost → prod). Header 768 réparé (flex:none + bascules 1024/1280/1360), og/favicon (+SSR), title suffixé. | VAGUE 3 (625b5e3) : 4 annuaires (Pages Vertes +127, Garde-Manger QC +97, Terroir et Saveurs +47, MIAM +21) → +283 boutiques (3 507), +99 connectables (1 568), +6 004 produits net (301 534) ; purge qualité provancher (4 150 billets de visites). Etsy 403 documenté. | 2026-08-19 : CAMPAGNE FRONT-END MOBILE (13 sites) — menus déroulants tactiles fiabilisés (verrous de scroll, backdrops portalisés hors headers backdrop-filter, hamburgers ajoutés à job-ka/trouve-ka/auto-ka), éléments du bas stabilisés (dvh, safe-area via viewport-fit=cover, échelle z-index --z-*), anti-zoom iOS ≥16 px, cibles tactiles ≥44 px, overflow-x clip html+body ; socle mobile commun dans ka-ui/tokens.css + ka-agent v2 ; audit+régression : ka-ui/mobile-audit.mjs & regression-mobile.mjs, inventaire+rapport : ka-ui/audit/. Commit poussé par app sur son nœud. | 2026-08-19 Phase 3 expansion: registre 3507->3947 boutiques (+440: CARTV bio/SIPAB 340, CTAQ 56, vendors collectifs 28, acheterquebecois-MTL 22, epipresto+laboiteagrains re-testés via curl), produits publiés 299k->347k (+48k); champ certifications (1679 fiches), statut « présumée fermée » (fail_streak>=12, 8 mortes dépubliées, réversible); canal auto-inscription /inscrire -> POST /api/inscriptions (pending, honeypot+rate-limit, admin X-Admin-Token=FABRIKA_ADMIN_TOKEN dans .env); docs/CONFORMITE.md (REQ écarté: Cloudflare+CC-BY-NC-SA; UPA app morte; Goûtez Lanaudière Eudonet 404). Commits 0199f91->be73a3f." | |
| 111 | + }, | |
| 112 | + { | |
| 113 | + "app": "food-ka", | |
| 114 | + "node": "M4M64b", | |
| 115 | + "port": 8097, | |
| 116 | + "domain": "www.food-ka.com", | |
| 117 | + "process_manager": "pm2", | |
| 118 | + "label_or_name": "food-ka-web / food-ka-sync / food-ka-ngrok", | |
| 119 | + "dir": "~/apps/food-ka", | |
| 120 | + "start": ".venv/bin/python run.py serve 8097 | .venv/bin/python run.py watch 360 | ~/bin/ngrok http --url=www.food-ka.com 8097", | |
| 121 | + "deployed": "2026-08-12", | |
| 122 | + "notes": "Agrégateur produits d'épicerie QC (20 connecteurs, ~22k produits). ATTENTION: utiliser ~/bin/ngrok (celui de homebrew se fige). Clés Scrapfly/Firecrawl dans .env. | 2026-08-13: SSO « Se connecter avec KA ID » via le hub groupe-ka.com — foodka/auth.py (JWT HS256 stdlib aud=food-ka, users keyed ka_id, cookie foodka_session signé SESSION_SECRET), AccountMenu header + « Mon compte » au menu mobile + page /profil carte de membre Groupe KA. Env node .env: + KA_SSO_SECRET/KA_HUB_URL/FOODKA_BASE_URL/SESSION_SECRET. Fix layout mobile ≤760px (compte à droite avant le hamburger). Testé bout-en-bout curl (ka_id partagé OK). | 2026-08-13: carte de membre UNIFORMISÉE — marque « Groupe KA » sur la carte (plus la marque de l'app), même gabarit que le hub. Déployé+vérifié dans le bundle prod. | 2026-08-13: PROFIL HUB AFFICHÉ — hubprofile.py (fetch signé /api/sso/profile du hub, cache 60s), /api/auth/me enrichi (bio/city/job_title/company/age/website/socials/role_label/public_url/profile_source), page profil: badge statut sur la carte + section « Mon profil Groupe KA » lecture seule + bouton « Modifier sur groupe-ka.com ». Testé bout-en-bout. | 2026-08-13 v2: FAVORIS ♥ hub — foodka/hubfav.py + routes /api/favorites + /toggle (502 explicite si hub muet), ♥ sur ProductCard + fiche (price-fav), favorites.tsx optimiste avec rollback, productFavItem (bannière·format, solde avec rég., /produit/{uid}), menu « Mes favoris ». Testé bout-en-bout. | 2026-08-17 harmonisation ka-ui : ACCENT VERT MARCHÉ #1f9d55 (fini le lime du hub), WIP SSO KA ID+favoris activé (buildé+déployé), badge, KaFooter, /contact, og/favicon, fix débordement 768 (bascule header 880 px). | 2026-08-18 CAMPAGNE CONNECTEURS (commits afdb2e6 + a45aeb4 + 114aa76) : pa_nature réparé (merchant 3287 + flyer_name_filter) ; Woo/Shopify enrichis (rating/poids/variantes/brands 0→88%), plafonds levés (+13,6k produits, aliments_merci complet) ; Flipp fine_print/discount/regular_price via fiches items ; 7 NOUVEAUX connecteurs circulaires grandes bannières (iga/metro/superc/maxi/provigo/walmart/costco_flyer, +3 103, zéro Scrapfly) ; MATCHING inter-bannières (product_links 1 248 groupes, /api/products/{uid}/compare) ; nutrition Open Food Facts (details.off, 100 rech./cycle) ; WAL+retry ; 48 760 produits actifs, 54 sources ; docs générées. | VAGUE 3 (2e52f7c+0e46a27) : SAQ SANS Scrapfly (Adobe Live Search GraphQL direct, clé publique storefront — 3 282 produits), 6 circulaires (pharmaprix/jean_coutu/brunet/uniprix filtrées rayons alimentaires, adonis/avril), metro +321 (coût mesuré 40-80 crédits/page). 52 865 actifs, 57 sources connectées, matching 1 518 groupes. | 2026-08-19 : CAMPAGNE FRONT-END MOBILE (13 sites) — menus déroulants tactiles fiabilisés (verrous de scroll, backdrops portalisés hors headers backdrop-filter, hamburgers ajoutés à job-ka/trouve-ka/auto-ka), éléments du bas stabilisés (dvh, safe-area via viewport-fit=cover, échelle z-index --z-*), anti-zoom iOS ≥16 px, cibles tactiles ≥44 px, overflow-x clip html+body ; socle mobile commun dans ka-ui/tokens.css + ka-agent v2 ; audit+régression : ka-ui/mobile-audit.mjs & regression-mobile.mjs, inventaire+rapport : ka-ui/audit/. Commit poussé par app sur son nœud." | |
| 123 | + }, | |
| 124 | + { | |
| 125 | + "app": "ka2", | |
| 126 | + "node": "M4M36", | |
| 127 | + "port": 8799, | |
| 128 | + "domain": "www.ka2.bot", | |
| 129 | + "process_manager": "launchd", | |
| 130 | + "label_or_name": "com.ka2.guardian + com.ka2.ngrok", | |
| 131 | + "dir": "~/cluster-projects/ka-guardian", | |
| 132 | + "start": "launchctl load ~/Library/LaunchAgents/com.ka2.guardian.plist (AGENT=ka2, ~/cluster-projects/ka-guardian, .venv python3.13) + com.ka2.ngrok conservé; runners com.ka.guardian-runner :7791 sur M3U96a/b+M4M64a/b; courrier com.ka.guardian-courier sur les 5 nœuds. Redeploy: deploy/deploy.sh all depuis ~/Desktop/Groupe-KA/apps-web/ka-guardian", | |
| 133 | + "deployed": "2026-08-23", | |
| 134 | + "notes": "ka2 GUARDIAN — RECONVERTI 2026-08-23 (ancien bot cartographie web QC archivé ~/ka-bots-backup-20260823.tar.gz sur M4M36). Agent gardien autonome des connecteurs: surveille api-ka /api/v1/monitoring/connectors (5 min), incidents broken/stale → mission claude -p headless (Claude Code CLI) via runner sur le nœud de l'app, surveillance 8 h post-fix, ROLLBACK git auto si ça empire (app down ou pas de guérison), 3 tentatives max. Périmètre: louka, immoka, restoka. + Efforts commandés depuis le site (nouveau connecteur avec découverte Serper + escalade Scrapfly/proxies/Apify, ou enrichissement d'un connecteur choisi) — sessions Claude Code 100% autonomes, jeton opérateur requis (~/.ka-guardian.env). Dashboard live style Groupe KA (papier/encre/lime): flux SSE, incidents, couverture, registre missions+transcripts. Source laptop ~/Desktop/Groupe-KA/apps-web/ka-guardian (git → spbgit ka-guardian.git). GOTCHAS: macOS 26 Local Network Privacy bloque le LAN dès que python homebrew est dans la chaîne (même ses enfants ssh/curl) → courrier zsh launchd (com.ka.guardian-courier, spool ~/ka-guardian-spool) qui expédie via ssh clé dédiée ka_guardian_ed25519 → curl localhost du nœud cible; launchd sans PATH; rsync silencieusement muet depuis $ROOT absolu → cd + ./ + vérif md5 dans deploy.sh. | 2026-08-23 (soir): bannières de partage OG par agent (/og.png + balises og:/twitter: rendues par agent dans main.py, jetons __NUM__/__DOMAIN__/__TAGLINE__) commit 810b430 ; le dir M4M36 est maintenant un REPO GIT (origin gitsrv via clé ka_guardian_ed25519) ; intégrés à administration-ka.com (chat projets ka2/4/6@M4M36 + Écosystème/actions launchd, admin-ka e379169). | 2026-08-28 : favicons corrigés — Safari n'affiche pas les SVG (onglet « ? ») → routes /favicon.ico (multi-tailles), /favicon.png 32 et /apple-touch-icon.png 180 par agent + fallbacks dans les 2 pages HTML." | |
| 135 | + }, | |
| 136 | + { | |
| 137 | + "app": "ka4", | |
| 138 | + "node": "M4M36", | |
| 139 | + "port": 8899, | |
| 140 | + "domain": "www.ka4.bot", | |
| 141 | + "process_manager": "launchd", | |
| 142 | + "label_or_name": "com.ka4.guardian + com.ka4.ngrok", | |
| 143 | + "dir": "~/cluster-projects/ka-guardian", | |
| 144 | + "start": "launchctl load ~/Library/LaunchAgents/com.ka4.guardian.plist (AGENT=ka4) — voir ka2", | |
| 145 | + "deployed": "2026-08-23", | |
| 146 | + "notes": "ka4 GUARDIAN — RECONVERTI 2026-08-23 (ancien bot V2 archivé). Même plateforme ka-guardian que ka2 (voir notes ka2). Périmètre: autoka, foodka, sortika. | 2026-08-23 (soir): bannières de partage OG par agent (/og.png + balises og:/twitter: rendues par agent dans main.py, jetons __NUM__/__DOMAIN__/__TAGLINE__) commit 810b430 ; le dir M4M36 est maintenant un REPO GIT (origin gitsrv via clé ka_guardian_ed25519) ; intégrés à administration-ka.com (chat projets ka2/4/6@M4M36 + Écosystème/actions launchd, admin-ka e379169). | 2026-08-28 : favicons corrigés — Safari n'affiche pas les SVG (onglet « ? ») → routes /favicon.ico (multi-tailles), /favicon.png 32 et /apple-touch-icon.png 180 par agent + fallbacks dans les 2 pages HTML." | |
| 147 | + }, | |
| 148 | + { | |
| 149 | + "app": "ka6", | |
| 150 | + "node": "M4M36", | |
| 151 | + "port": 8999, | |
| 152 | + "domain": "www.ka6.bot", | |
| 153 | + "process_manager": "launchd", | |
| 154 | + "label_or_name": "com.ka6.guardian + com.ka6.ngrok", | |
| 155 | + "dir": "~/cluster-projects/ka-guardian", | |
| 156 | + "start": "launchctl load ~/Library/LaunchAgents/com.ka6.guardian.plist (AGENT=ka6) — voir ka2", | |
| 157 | + "deployed": "2026-08-23", | |
| 158 | + "notes": "ka6 GUARDIAN — RECONVERTI 2026-08-23 (ancien bot flagship créateurs archivé). Même plateforme ka-guardian que ka2 (voir notes ka2). Périmètre: fabrika, jobka, creaka + tout nouveau service api-ka (agent par défaut). | 2026-08-23 (soir): bannières de partage OG par agent (/og.png + balises og:/twitter: rendues par agent dans main.py, jetons __NUM__/__DOMAIN__/__TAGLINE__) commit 810b430 ; le dir M4M36 est maintenant un REPO GIT (origin gitsrv via clé ka_guardian_ed25519) ; intégrés à administration-ka.com (chat projets ka2/4/6@M4M36 + Écosystème/actions launchd, admin-ka e379169). | 2026-08-28 : favicons corrigés — Safari n'affiche pas les SVG (onglet « ? ») → routes /favicon.ico (multi-tailles), /favicon.png 32 et /apple-touch-icon.png 180 par agent + fallbacks dans les 2 pages HTML." | |
| 159 | + }, | |
| 160 | + { | |
| 161 | + "app": "trouve-ka", | |
| 162 | + "node": "M2M32", | |
| 163 | + "port": 3000, | |
| 164 | + "domain": "www.trouve-ka.com", | |
| 165 | + "process_manager": "pm2 (natif) + docker (PG/Redis) + colima", | |
| 166 | + "label_or_name": "pm2: tk-web/tk-api/tk-crawler-1,2/tk-enrichment/tk-scheduler + docker: postgres,redis + com.trouveka.ngrok", | |
| 167 | + "dir": "~/trouve-ka", | |
| 168 | + "start": "colima démarré via brew services; cd ~/trouve-ka && docker compose up -d; ngrok via launchctl load ~/Library/LaunchAgents/com.trouveka.ngrok.plist", | |
| 169 | + "deployed": "2026-08-13", | |
| 170 | + "notes": "Moteur de recherche Québec-first (crawler+index OpenSearch). Web :3000 proxifie /api vers api:8080. Seeds: docker compose run --rm --entrypoint python crawler-worker -m trouveka.crawler.seed scripts/bootstrap-seeds/seeds.txt. Source: ~/Desktop/trouve-ka sur le laptop. | Workers satellites de crawl (2026-08-13) : M2M32b (pm2 trouveka-crawler), M2M32c, M4BP48, M1M32, m4mc, m4ma (nohup .venv/bin/python -m trouveka.crawler.worker, ~/trouve-ka, log ~/trouveka-crawler.log) → Postgres/Redis/OpenSearch via LAN 192.168.2.90 (ports 5432/6379/9200; ne PAS écraser le .env du node M2M32 : PG_PORT=5432, REDIS_PORT=6379). Postgres compose : max_connections=200. Relance satellites depuis session SSH (jamais launchd). | 2026-08-15 REFONTE : colima M2M32 a perdu l'accès container→LAN/host → API/web/workers/enrichment/scheduler passés en pm2 NATIF sur M2M32 (docker=PG+Redis seulement). OpenSearch migré sur M2M32b:9200 (colima démarré via SSH, heap 8G, index trouveka-docs-v2 2 shards kNN, ancien index conservé stoppé sur M2M32). Embeddings/rerank: m4mc:8091. Satellites → SEARCH_URL 192.168.2.77:9200. | 2026-08-17 harmonisation ka-ui : tk-web = Next 15 apps/web, rethémé papier/encre ACCENT AZUR #1c7ed6 (mode sombre retiré), badge+KaFooter+/contact + SSO KA ID IMPLÉMENTÉ (apps/web/lib/ka-auth.ts, cookie tk_ka_session, .env racine chargé via next.config), og/favicon. | 2026-08-18 (commit bd94129) : 13 domaines Ka en seeds, 442 échecs requeued, 29 448 URLs de sitemaps Ka enfilées (enqueue-sitemap.py), 0 parse_failed (SSR ok) — lou-ka 42→239 docs, immo-ka 1→200, indexation continue. food-ka/crea-ka = SPA sans sitemap (vides sans rendu JS). | 2026-08-19 : CAMPAGNE FRONT-END MOBILE (13 sites) — menus déroulants tactiles fiabilisés (verrous de scroll, backdrops portalisés hors headers backdrop-filter, hamburgers ajoutés à job-ka/trouve-ka/auto-ka), éléments du bas stabilisés (dvh, safe-area via viewport-fit=cover, échelle z-index --z-*), anti-zoom iOS ≥16 px, cibles tactiles ≥44 px, overflow-x clip html+body ; socle mobile commun dans ka-ui/tokens.css + ka-agent v2 ; audit+régression : ka-ui/mobile-audit.mjs & regression-mobile.mjs, inventaire+rapport : ka-ui/audit/. Commit poussé par app sur son nœud." | |
| 171 | + }, | |
| 172 | + { | |
| 173 | + "app": "api-ka", | |
| 174 | + "node": "M3U96b", | |
| 175 | + "port": 8000, | |
| 176 | + "domain": "www.api-ka.com", | |
| 177 | + "process_manager": "pm2", | |
| 178 | + "label_or_name": "apika-api + apika-scheduler + apika-ngrok", | |
| 179 | + "dir": "/opt/api-ka", | |
| 180 | + "start": "cd /opt/api-ka && pm2 start ./venv/bin/uvicorn --name apika-api --interpreter none -- src.api.main:app --host 127.0.0.1 --port 8000 ; pm2 start ./venv/bin/python --name apika-scheduler --interpreter none -- -m src.scheduler.daily_job ; pm2 start /opt/homebrew/bin/ngrok --name apika-ngrok --interpreter none -- http --domain=www.api-ka.com 8000 --log stdout", | |
| 181 | + "deployed": "2026-08-16", | |
| 182 | + "notes": "API-KA — plateforme centrale de l'écosystème KA. Collecte QUOTIDIENNE (02:00, APScheduler) des 5 services KA vers PostgreSQL 16 local (base apika, user apika_user, localhost only), API publique FastAPI (/api/v1/{louka,immoka,foodka,autoka,fabrika} + /runs + /health, pagination max 500, rate limit 120/min/IP), backfill auto 7 jours, backups pg_dump horodatés /opt/api-ka/data/backups (rétention 90j, cron 03:30 + backup par run). Sources via LAN inter-node (ngrok public non requis): lou-ka 127.0.0.1:8095 (même node), immo-ka/fabri-ka MacStudio-de-simon-pierre-6.local:8096/8097 (M4M64a), auto-ka/food-ka MacStudio-de-simon-pierre-4.local:8095/8097 (M4M64b) — collecteurs paginés (limit/offset, fabri-ka page/per_page). IMPORTANT: HostName kernel du node réglé à m3u96b (scutil --set HostName, 2026-08-16) car verify_node() exige hostname==m3u96b en production (LocalHostName/ComputerName inchangés). .env dans /opt/api-ka/.env (600, jamais commité). pm2 save fait. Source laptop: ~/Desktop/api-ka. Tests: 24 pytest pass. En-tête auteur Simon-Pierre Boucher — contact@spboucher.ai dans chaque fichier. | 2026-08-16: PLATEFORME WEB de documentation + playground servie sur www.api-ka.com (src/api/web/index.html, négociation de contenu sur GET /: text/html → plateforme, sinon statut JSON). Design initial éditorial REMPLACÉ le 2026-08-16 par le thème LOU-KA (demande utilisateur): Inter 700/800 serré, blanc + navy #0b1330 + orange #ff6a00 (tokens extraits du CSS lou-ka: r-card 18px, stat-chips pilule avec point pulsant orange, btn orange à lueur, pilules GET blue-soft #e9efff, terminaux code navy, footer navy), tile héros navy avec /health live colorisé, reveal on scroll, sections: héros stats live, 5 cartes services (compteurs live), référence des 7 endpoints (tables paramètres + snippets curl/Python/JS + copier), playground interactif (champs dynamiques par endpoint, statut+latence+taille), conventions. Première collecte complète réussie le jour du déploiement. | 2026-08-17 : 3 nouveaux services collectés — restoka (RESTOKA_SOURCE_URL=127.0.0.1:8115/api/restaurants, fetch surchargé : liste paginée PUIS fiche détaillée /api/restaurants/{uid} pour sauvegarder les PLATS — menus/sections/items avec options et prix ; 542 restos, 541 avec menus), sortika (SORTIKA_SOURCE_URL=MacStudio-de-simon-pierre-2.local:8120/api/events — M3U96a via LAN, 8 989 événements), creaka (CREAKA_SOURCE_URL=127.0.0.1:8160/api/creators, 2 233 créateurs). Tables restoka_data/sortika_data/creaka_data créées, première collecte OK, /api/v1/{restoka,sortika,creaka} en ligne, web docs '8 services'. 27 tests pass. Repo créé : git.spboucher.ai/api-ka (spbgit create). | 2026-08-17 design Groupe KA appliqué (site docs statique src/api/web/, accent indigo #3b5bdb, ka-ui monté sur /ka, ka-shell.js ← ecosystem.json) + SSO KA ID IMPLÉMENTÉ (src/api/routes/auth.py, cookie apika_session, client api-ka au hub), /contact, og/favicon (routes FileResponse). | 2026-08-17 (2) : page /stats (dashboard vanilla SVG + journalisation api_requests tampon 5 s + backfill logs) + PDF ; GET /api/stats/ecosystem-report (ecopdf.py — PDF consolidé des plateformes, cache 10 min, consommé par le hub). | 2026-08-17 (3) KA AGENT : assistant IA central de l'écosystème (Claude Haiku 4.5, clé ANTHROPIC_API_KEY dans le .env du nœud) — POST /api/agent/chat en SSE (deltas texte + événements outils), boucle manuelle 6 tours max, 12 outils sur les API publiques (recherche par plateforme, stats live, état des services, infos écosystème), prompt par site avec cache, CORS 13 domaines. Widget ka-ui/agent/ka-agent.js (bulle → panneau → plein écran, historique localStorage) câblé sur tous les sites. Testé bout-en-bout (vraies annonces + stats streamées). | 2026-08-18 (commit 02c0fc8) : cron backup 03:30 réparé (pg_dump chemin absolu, 8/8 testés + purge 90 j) ; pytest isolé des logs prod ; supervision centralisée des connecteurs en cours d'ajout (connector_health + /api/v1/monitoring/connectors + alertes). | 2026-08-19 KA AGENT v2 : widget ka-agent.js (les 13 sites) — plein écran à l'ouverture, clavier mobile stable (visualViewport + body fixed), Markdown streaming complet ; backend /api/agent/chat (apika-api) — 24 outils (fiche_detail par uid, comparer_prix_epicerie, rappels_vehicule, inspections_resto, menu_resto, juste_prix_logement, estimer_valeur_propriete, chercher_plats/boutiques, suggestions_recherche, facettes_plateforme) + recherche floue en cascade (accents/mots vides/singulier), max_tokens 2000, MAX_TURNS 8. | 2026-08-19 : CAMPAGNE FRONT-END MOBILE (13 sites) — menus déroulants tactiles fiabilisés (verrous de scroll, backdrops portalisés hors headers backdrop-filter, hamburgers ajoutés à job-ka/trouve-ka/auto-ka), éléments du bas stabilisés (dvh, safe-area via viewport-fit=cover, échelle z-index --z-*), anti-zoom iOS ≥16 px, cibles tactiles ≥44 px, overflow-x clip html+body ; socle mobile commun dans ka-ui/tokens.css + ka-agent v2 ; audit+régression : ka-ui/mobile-audit.mjs & regression-mobile.mjs, inventaire+rapport : ka-ui/audit/. Commit poussé par app sur son nœud." | |
| 183 | + }, | |
| 184 | + { | |
| 185 | + "app": "crea-ka", | |
| 186 | + "node": "M3U96b", | |
| 187 | + "port": 8160, | |
| 188 | + "domain": "www.crea-ka.com", | |
| 189 | + "process_manager": "pm2", | |
| 190 | + "label_or_name": "crea-ka-web + crea-ka-sync + crea-ka-ngrok", | |
| 191 | + "dir": "~/apps/crea-ka", | |
| 192 | + "start": "cd ~/apps/crea-ka && pm2 start .venv/bin/python --name crea-ka-web --interpreter none --cwd ~/apps/crea-ka -- run.py serve 8160 ; PYTHONUNBUFFERED=1 pm2 start .venv/bin/python --name crea-ka-sync --interpreter none --cwd ~/apps/crea-ka -- run.py watch 24 ; pm2 start /opt/homebrew/bin/ngrok --name crea-ka-ngrok --interpreter none -- http --url=www.crea-ka.com 8160", | |
| 193 | + "deployed": "2026-08-17", | |
| 194 | + "notes": "Créa-Ka — annuaire public des créateurs de contenu québécois (crea-ka.com), architecture calquée sur Lou-Ka (FastAPI+SQLite WAL+frontend statique zéro build, identité visuelle Lou-Ka marine/#2258FF/orange). Pipeline: découverte (listes-medias: dataset 376 créateurs de ~/Desktop/qc-influenceur; wikidata-qc: SPARQL Québec) → enrichissement par plateforme créateur-par-créateur (instagram-profil + tiktok-profil via Scrapfly [SCRAPFLY_KEY dans ~/apps/crea-ka/.env], youtube API/page publique, twitch Helix sauté sans clés, link-in-bio Linktree/Beacons signal 0.95) → résolution d'identité (scores §12.1) → dédup par compte fort partagé (jamais sur le nom — homonymes). 382 créateurs/605 comptes au 1er passage, enrichissement complet en cours au déploiement. Éthique §15: scrub données privées, opt-out /retrait (immédiat+persistant data/optout.json), mineurs exclus de l'annuaire public (is_minor auto via année de naissance Wikidata). Routes: / annuaire filtres niche/région/langue/plateforme/tier, /createur/{id} page dédiée par créateur, /retrait, API /api/creators|stats|taxonomies|sources|optout, /healthz. OnlyFans supporté comme plateforme (liens auto-déclarés seulement). Tests: 25 pass (pytest, aussi sur le node). Redéploiement: scripts/deploy-m3u96b.sh (⚠️ premier rsync du script n'avait rien copié — vérifier). watch 24h = cadence quotidienne. Node imposé par l'utilisateur. pm2 save fait. Source laptop: ~/Desktop/crea-ka. En-tête auteur Simon-Pierre Boucher — contact@spboucher.ai partout. | 2026-08-17 v2: refonte visuelle « éditorial sharp » Groupe KA (papier grainé #f5f3ee, encre #141814, accent VIOLET électrique #c9a3ff/#5b34a8, Space Grotesk/Inter/JetBrains Mono, bordures encre + ombres décalées, vraies icônes SVG simple-icons injectées au build par scripts/build_frontend.py depuis frontend/src/{index.template.html,icons.json}, tab bar mobile 4 onglets patron Lou-Ka, cartes photo unavatar+avatar_url avec cascade de replis, page /stats tuiles+barres). +5 connecteurs: onlyqueb (sitemap+JSON-LD, 248 profils 18+, liens auto-déclarés), youtube-recherche (~120 requêtes QC filtrées par marqueur, ~1300 chaînes/passage), balados-itunes (API officielle Apple, ~400 balados), + metrics étendues par compte (posts/likes/vues/épisodes, colonne accounts.metrics) et avatar_url capté à la source (IG hd/TikTok/artwork iTunes). ~2200+ créateurs / 2800+ comptes après 1er passage complet. ⚠️ PIÈGE openrsync macOS: 'NODE:chemin/relatif' = chemin LOCAL silencieux — le deploy script utilise désormais le chemin distant ABSOLU. Repo: https://git.spboucher.ai/crea-ka.git | 2026-08-17 v3 PROFONDEUR: youtube-recherche paginé par continuations INNERTUBE (4 pages/requête, ~5x le rendement — 607 requêtes incl. 56 villes/MRC et ~100 passe-temps/métiers ×QC pour la longue traîne nano/micro), balados-itunes 87 requêtes, plafonds enrichissement YT 1500/IG 700/TikTok 700/linkinbio 500 par passage. Fixes: parse_count lisait un handle '@…tw5bu' comme 5 G abonnés (frontières de mot + rejet des @) ; IDs de chaîne YouTube UC… casse préservée + URL /channel/ (35 comptes cassés purgés de la DB node). Commit e09cc05 poussé sur git.spboucher.ai/crea-ka. | 2026-08-17 v4 KA ID: connexion déléguée au hub groupe-ka.com — creaka/auth.py (state signé, JWT HS256 vérifié stdlib aud=crea-ka, cookie creaka_session SANS table users, /api/me enrichi par /api/sso/profile du hub signé HMAC cache 60s, logout/config), frontend bouton compte header (visible mobile) + page /compte carte de membre Groupe KA encre+lime. .env node: +KA_SSO_SECRET/SESSION_SECRET/CREAKA_BASE_URL/KA_HUB_URL. Hub: client crea-ka ajouté à src/lib/sso.ts + KA_SSO_SECRET_CREA_KA dans ~/apps/groupe-ka/.env, hub rebuildé+redémarré. Testé bout-en-bout curl en prod. Commits: crea-ka 0b6ebec, groupe-ka a656d95. | 2026-08-17 harmonisation ka-ui : ACCENT VIOLET #7048e8, tokens/ecosystem injectés au build (__KA_TOKENS__/__KA_ECO__ dans build_frontend.py), badge, footer KaFooter, /contact, og/favicon, fix débordements header (bascules 940/1040 px). | 2026-08-18 CAMPAGNE CONNECTEURS (commits 154742b + 6d3e57d) : fix 500 threads SQLite, seconds réels, SkipSource twitch ; OnlyQueb RSC (bio 15→98%, vrais noms, mym/fansly) ; avatars youtube (backfill 4 040) ; balados feed_url/dormants (223 inactive) ; archivage §13 implémenté ; NOUVEAUX : balados-rss (204 enrichis, site-web +70), podcastindex prêt (clés requises), spotify/discord, followers X via syndication timeline-profile (gotcha TLS python → curl), bio-liens ; 14 sources, 19 plateformes ; clés à fournir : YOUTUBE_API_KEY, TWITCH_*, FIRECRAWL, PODCASTINDEX ; docs générées. | VAGUE 3 SANS CLÉ (e97730b) : Twitch via Client-ID public web (21/26 chiffrés), stats YouTube via InnerTube (264 chaînes/passage, rotation 14 j), wikipedia-qc via Wikidata (+76 fiches curées, site-web 87→172), palmares-apple. BUG purgé : marqueurs QC sans frontières de mot (television→levis). 16 sources, 5 513 actifs. | 2026-08-18 VAGUE 4 EXPANSION MASSIVE (commit a4fcfae) : 5 841 → 9 437 créateurs (+3 596), 10 512 comptes. NOUVEAUX connecteurs : twitch-decouverte (GQL public searchFor 43 termes QC paginé + répertoire streams FR 20 pages, +262 streamers, avatars/partner à la source), kick-decouverte (Scrapfly ASP — kick.com 403 direct ; api/search 27 termes + fiche v2 avec LIENS SOCIAUX AUTO-DÉCLARÉS → cross_link 0.90, +119 créateurs/188 comptes, cap 120 v2/passage), region-bio (0 requête : ville DÉCLARÉE en bio via tournure locative/gentilé → région §7, ~70 villes sûres, abstention si ambigu — 1 391 fiches localisées d'un coup). EXPANSIONS : youtube-recherche 641→1 361 requêtes (bloc anglophone MTL/QC, 13 thèmes×56 villes, ~100 nouveaux passe-temps/métiers ; +3 180 chaînes, passage 2 h 17), balados-itunes 87→146 requêtes (+27), wikipedia-qc +5 catégories vérifiées (Blogueuse QC, CA Internet celebrities/women podcasters/video bloggers/gaming YouTubers ; +8, 112 enrichies). Avatars 96,5 % (9 105), régions 1 544. 19 sources, 46 tests OK. x-profil 122 erreurs (TLS/429 syndication — repli curl, à surveiller). | 2026-08-19 : CAMPAGNE FRONT-END MOBILE (13 sites) — menus déroulants tactiles fiabilisés (verrous de scroll, backdrops portalisés hors headers backdrop-filter, hamburgers ajoutés à job-ka/trouve-ka/auto-ka), éléments du bas stabilisés (dvh, safe-area via viewport-fit=cover, échelle z-index --z-*), anti-zoom iOS ≥16 px, cibles tactiles ≥44 px, overflow-x clip html+body ; socle mobile commun dans ka-ui/tokens.css + ka-agent v2 ; audit+régression : ka-ui/mobile-audit.mjs & regression-mobile.mjs, inventaire+rapport : ka-ui/audit/. Commit poussé par app sur son nœud." | |
| 195 | + }, | |
| 196 | + { | |
| 197 | + "app": "sorti-ka", | |
| 198 | + "node": "M3U96a", | |
| 199 | + "port": 8120, | |
| 200 | + "domain": "www.sorti-ka.com", | |
| 201 | + "process_manager": "pm2", | |
| 202 | + "label_or_name": "sorti-ka-web + sorti-ka-sync + sorti-ka-ngrok", | |
| 203 | + "dir": "~/apps/sorti-ka", | |
| 204 | + "start": "cd ~/apps/sorti-ka && pm2 start .venv/bin/python --name sorti-ka-web --interpreter none -- run.py web --host 0.0.0.0 --port 8120 ; pm2 start .venv/bin/python --name sorti-ka-sync --interpreter none -- run.py watch --interval 3600 ; pm2 start /opt/homebrew/bin/ngrok --name sorti-ka-ngrok --interpreter none -- http --url=www.sorti-ka.com 8120", | |
| 205 | + "deployed": "2026-08-17", | |
| 206 | + "notes": "Sorti-Ka — agrégateur de sorties & événements du Québec (famille ·Ka, patron Lou-Ka : paquet sortika/ = schema Event + normalize + db SQLite WAL (grâce 2 syncs, dérive, busy_timeout 60s) + connecteurs auto-découverts + FastAPI + SSR SEO + frontend SPA sans build). 15 231 événements actifs / 11 559 à venir / 3 159 gratuits / 17 régions / 428 villes. 8 connecteurs ACTIFS : lepointdevente (billetterie QC ~2 500, listes HTML, robots Allow *, hors-QC exclus), sitq (MTO Tourinsoft CC 4.0, toutes régions + enrichissement og:image des sites officiels), montreal (CKAN CC BY 4.0 ~5 600 + enrichissement og:image des fiches montreal.ca — ⚠ WAF bloque UA contenant 'bot' → UA SortiKa/1.0), evenko (~460 grands concerts — /api/search permis par robots, format PERCÉ : GET ?body=base64({params:[query,options],lang:'fr-CA'}) + filters entity_type:evenko_show ; lien billets = Ticketmaster), lavitrine (~5 100 représentations — sitemap + fiches, ⚠ le carrousel d'une fiche de tournée liste TOUTES les dates de la tournée → identifier chaque représentation par l'id de SA slide, sinon 47 doublons/spectacle), atuvu (Cloudflare → Scrapfly, cache incrémental plafonné ATUVU_MAX_FETCH=120/sync, rattrapage progressif), sherbrooke (~270 — JSON embarqué window['TRANSFER_CACHE'], images /Fichiers/3337a882…/Entities/<id>/<mainPicture>), laval (~290, external_id slug+date). Enrichissements d'images PARALLÈLES (fetch_many pool threads). Ville→région : Répertoire MAMH (MUN+ARR, 1 250 entrées, homonymes par population). Dédup affichage : title+ville+date, fiche la plus riche gagne (fenêtre SQL ROW_NUMBER). Fiches /evenement/{uid} SSR (JSON-LD Event) + sitemaps + robots. Frontend design system Groupe KA « éditorial sharp ». Watcher HORAIRE. 19 tests pytest fixtures réelles. Sources écartées (périmées) : Longueuil 2017, Saguenay 2022, Repentigny 2021. Scrapfly : SCRAPFLY_API_KEY dans .env. Repo : https://git.spboucher.ai/sorti-ka (bare ~/srv/git/sorti-ka.git + meta.json). README avec pastilles + screenshots docs/screenshots/. Node imposé par l'utilisateur malgré la règle M3U96a=staging. pm2 save fait. Source laptop : ~/Desktop/sorti-ka. En-tête auteur Simon-Pierre Boucher — contact@spboucher.ai partout. | 2026-08-17 v1.1 : connexion KA ID (« Se connecter avec KA », sortika/auth.py patron louka/auth.py — SSO hub groupe-ka.com, jeton HS256 secret partagé KA_SSO_SECRET, session cookie httpOnly, /api/me avec profil dicté par le hub, table users, bouton + puce membre dans l'en-tête). ⚠ IMPORTANT : le hub groupe-ka (M3U96b ~/apps/groupe-ka) n'avait AUCUN secret SSO en prod (.env absent → « Secret SSO non configuré » pour TOUTES les plateformes) — .env RESTAURÉ le 2026-08-17 avec les 7 secrets (lou-ka, toit-ka, immo-ka, auto-ka, food-ka, ora-ka récupérés des .env de chaque app + sorti-ka généré), client sorti-ka inscrit dans src/lib/sso.ts, rebuild + pm2 restart groupe-ka. Secrets sorti-ka dans ~/apps/sorti-ka/.env (SESSION_SECRET, KA_SSO_SECRET, SORTIKA_BASE_URL). 22 tests. | 2026-08-17 harmonisation ka-ui : ACCENT FRAMBOISE #d6336c (SPA sans build — tokens.css servi statiquement), badge, footer commun, /contact (+SSR seo.py), og/favicon (+SSR, fiches gardent leur photo), header flex-wrap (fix 768). | 2026-08-18 CAMPAGNE CONNECTEURS (commits 7d9d1b0 + 65d2d33) : venue/adresse montréal, prix billetterie lepointdevente (/plugins/rates : prix 0→124+, desc 0→129), descriptions sitq 0→87% (tél/courriel), GÉOCODEUR de salles Nominatim (405 événements géolocalisés, cache) ; colonnes start_time (3 069, TZ corrigée laval/sherbrooke UTC) + artists (348) ; NOUVEAUX : bandsintown ACTIF (+108, app_id widget), brossard (+251), ticketmaster prêt (clé requise) ; fusion doublons au barème richesse (545 groupes) ; /api/stats expose sources:10 ; docs générées. | VAGUE 3 SANS CLÉ (86b1494+e312e16) : Ticketmaster par scraping DIRECT (/discover __NEXT_DATA__, 330 évén. 100% GPS, 0 crédit, repli API auto si clé), Eventbrite (426 évén. + prix JSON-LD), longueuil (Nuxt) + cantonsdelest (Tourinsoft). +778 actifs, 20 sources. | 2026-08-19 : CAMPAGNE FRONT-END MOBILE (13 sites) — menus déroulants tactiles fiabilisés (verrous de scroll, backdrops portalisés hors headers backdrop-filter, hamburgers ajoutés à job-ka/trouve-ka/auto-ka), éléments du bas stabilisés (dvh, safe-area via viewport-fit=cover, échelle z-index --z-*), anti-zoom iOS ≥16 px, cibles tactiles ≥44 px, overflow-x clip html+body ; socle mobile commun dans ka-ui/tokens.css + ka-agent v2 ; audit+régression : ka-ui/mobile-audit.mjs & regression-mobile.mjs, inventaire+rapport : ka-ui/audit/. Commit poussé par app sur son nœud. Phase 3 (2026-08-19) : expansion des sources — 9 nouveaux connecteurs (lavalweb ~470 évts vivants remplaçant Laval morte, outaouais, lanaudiere, saguenaysorties, bassaintlaurent, quebecanimee, placedesarts avec séances+prix réels, lhjmq ~380 matchs QC, sallesjsonld générique Grand Théâtre/Trident/Capitole) + annuaire de salles (table venues : auto-apprentissage + OSM Overpass + Nominatim) + déroulage des périodes multiples SITQ ; écartées consignées : mauricie/laurentides/gaspesie (WAF/JS), tuxedo (panne), clubs pros (doublons). docs/CONFORMITE.md créé. Rapport : ~/Desktop/groupe-ka-connecteurs/moine-2026-08-19/sorti-ka/06-EXPANSION-RAPPORT.md" | |
| 207 | + }, | |
| 208 | + { | |
| 209 | + "app": "resto-ka", | |
| 210 | + "node": "M3U96b", | |
| 211 | + "port": 8115, | |
| 212 | + "domain": "www.resto-ka.com", | |
| 213 | + "process_manager": "pm2", | |
| 214 | + "label_or_name": "resto-ka + resto-ka-ngrok + resto-ka-sync", | |
| 215 | + "dir": "~/apps/resto-ka", | |
| 216 | + "start": "cd ~/apps/resto-ka && pm2 start .venv/bin/python --name resto-ka --interpreter none -- run.py serve 8115 ; pm2 start /opt/homebrew/bin/ngrok --name resto-ka-ngrok --interpreter none -- http --url=www.resto-ka.com 8115 ; pm2 start .venv/bin/python --name resto-ka-sync --interpreter none -- run.py watch 168", | |
| 217 | + "deployed": "2026-08-17", | |
| 218 | + "notes": "Resto·Ka — agrégateur des restaurants du Québec (menus complets + prix réels), resto-ka.com. v2 (design) puis v3 (couverture) 2026-08-17 : 542 restos / 61 330 plats (98% avec photo, 69% avec options+suppléments) / 14 régions / 15 chaînes — 19 intégrations UEAT (Sushi Shop 141, Poulet Rouge 82, Valentine 76, Normandin 44, Chocolats Favoris 41, Allô mon Coco 34, Thaïzone 26, Scores 25, Ashton 23, Pacini 20, Tutti Frutti, Mandy's, Panda Boba-T, Ryu, Impérial, Pizza Mia, Station W, Le Super Qualité, Château Frontenac). GUID des chaînes découverts via <marque>.order-online.ai (domaine marque blanche UEAT). Design v2 « éditorial sharp » Groupe KA (Space Grotesk/Inter/JetBrains Mono, papier-encre-lime, bordures encre + ombres dures) — demande utilisateur : cohérence avec toutes les apps du groupe. Recherche PAR PLAT (/api/dishes, groupée par marque), pagination numérotée, images plats/logos, options en pastilles. Backend Python/FastAPI+SQLite calqué Lou·Ka (price_context/price_source obligatoires, menus par contexte, historique prix par item, alertes dérive). resto-ka-sync = run.py watch 168 (hebdo). St-Hubert consigné 'à faire' (Akamai). Repo: https://git.spboucher.ai/resto-ka (commit initial poussé). Tests 41 pass. Port 8115 (8110=groupe-ka). Node imposé par l'utilisateur. pm2 save fait. Source laptop: ~/Desktop/resto-ka. En-tête auteur Simon-Pierre Boucher <contact@spboucher.ai> partout. | v3 MÊME JOUR (demande: « le plus grand agrégateur ») : connecteur découverte OSM/Overpass (aire 3600061549, ODbL, attribution au footer) = 13 531 fiches nommées → TOTAL 14 055 restos actifs / 79 159 plats / 17/17 régions / 738 restos avec menu complet / 24 chaînes. +10 intégrations UEAT (Boustan 65, Ben&Florentine 54, NO.900 32, Sushi Taxi 30, Cochon Dingue, Salvatoré, Belle Province, Madisons, Freddy) = 29. Dédup chaîne↔OSM (215 masqués, suffixe de succursale retranché dans _name_key, osm dernier en SOURCE_PRIORITY). Régions par ancres géographiques (17/17). UI: filtre has_menu, tri menus d'abord. 45 tests. Commit v2 poussé (git.spboucher.ai/resto-ka). | 2026-08-17 harmonisation ka-ui : ACCENT SAFRAN #f08c00 (fini le lime), tokens vendorés, badge, KaFooter, /contact (whitelist web.py), og/favicon, title suffixé. | 2026-08-18 CAMPAGNE CONNECTEURS (commits 21c54c2 + 85873e9) : anti-fausses-fermetures (7 354 miss_count reset + garde bootstrap ; CAUSE RACINE : miroir overpass.osm.ch = extrait SUISSE, retiré) ; UEAT phone 0→99,8% + GPS API 100% + 20 GUID (+113 fiches, 50 intégrations) ; OSM brand→chain 18%, services, 45 cuisines ; site-resto DÉBLOQUÉ (paquet anthropic manquant) — menus Haiku en prod ; NOUVEAU MAPAQ (2 994 condamnations, 288 liées/204 restos, /inspections) ; horaires OSM structurés 90,4% ; tél/résa des sites crawlés ; Yelp prêt (clé requise) ; Overpass 406 résolu (UA bot honnête) ; docs générées. | VAGUE 3 SANS CLÉ (a9e4fb2) : avis Yelp par scraping (cache Apollo de la page recherche, 1 page/resto, ~30 crédits — 42 notés, 80/cycle), menus livraison UberEats (index 37 087 slugs, ~1 crédit/page, 13 menus delivery), permis d'alcool RACJ croisés sur 1 924 restos (CC-BY). 9 sources. | 2026-08-19 : CAMPAGNE FRONT-END MOBILE (13 sites) — menus déroulants tactiles fiabilisés (verrous de scroll, backdrops portalisés hors headers backdrop-filter, hamburgers ajoutés à job-ka/trouve-ka/auto-ka), éléments du bas stabilisés (dvh, safe-area via viewport-fit=cover, échelle z-index --z-*), anti-zoom iOS ≥16 px, cibles tactiles ≥44 px, overflow-x clip html+body ; socle mobile commun dans ka-ui/tokens.css + ka-agent v2 ; audit+régression : ka-ui/mobile-audit.mjs & regression-mobile.mjs, inventaire+rapport : ka-ui/audit/. Commit poussé par app sur son nœud." | |
| 219 | + }, | |
| 220 | + { | |
| 221 | + "app": "job-ka", | |
| 222 | + "node": "M3U96a", | |
| 223 | + "port": 8096, | |
| 224 | + "domain": "www.job-ka.com", | |
| 225 | + "process_manager": "pm2", | |
| 226 | + "label_or_name": "job-ka-web + job-ka-sync + job-ka-ngrok", | |
| 227 | + "dir": "~/apps/job-ka", | |
| 228 | + "start": "cd ~/apps/job-ka && pm2 start '.venv/bin/python run.py serve 8096' --name job-ka-web ; pm2 start '.venv/bin/python run.py watch 60' --name job-ka-sync ; pm2 start /opt/homebrew/bin/ngrok --name job-ka-ngrok --interpreter none -- http --url=www.job-ka.com 8096", | |
| 229 | + "deployed": "2026-08-17", | |
| 230 | + "notes": "Job·Ka — agrégateur d'offres d'emploi québécoises du Groupe KA (Python/FastAPI + SQLite + React/Vite, architecture héritée de Lou·Ka). 135 connecteurs sur 9 plateformes ATS (Workday avec facette Québec auto-découverte, Lever, Greenhouse, SmartRecruiters, Ashby, Workable, Recruitee, Breezy, BambooHR), ~4200+ offres actives au déploiement. Salaires normalisés $/h↔$/an, géocodage Nominatim+Adresses Québec, dédup union-find, anti-dérive, expiration date limite. watch 60 = resync horaire autonome (le nœud maintient ses données lui-même, data/jobka.db jamais rsyncé). Healthcheck /health. Générateur de connecteurs: scripts/gen_connectors.py + data/feeds*.json. Nœud M3U96a imposé par le CLAUDE.md du projet (§6, dérogation staging assumée). pm2 save fait. Source laptop: ~/Desktop/job-ka (git local, 3 commits). Clé Scrapfly dans .env sur le nœud (non versionnée). Auteur: Simon-Pierre Boucher — contact@spboucher.ai. | 2026-08-17 INTÉGRATION ÉCOSYSTÈME : repo git remote-first (bare spbgit job-ka.git + git sur le nœud), 13e site d'ecosystem.json (accent CYAN #0c8599), client SSO job-ka enregistré au hub (secret dans .env), assets og/favicon générés. Harmonisation ka-ui + stats+PDF en cours par agent (badge, KaFooter, /contact, /stats, dashboard, rapport PDF). | 2026-08-18 CAMPAGNE CONNECTEURS JOB-KA (commits fd4bfc8, 5a66f48, 4917d57, f6e7571, 744cbfc, 1604ff9 + consolidation) : employeurs directs 135→~250 (probe_ats.py ~870 slugs/14 500 req + detect_ats.py sur 6 337 domaines de l'écosystème — liste /tmp/jobka-employeurs) ; percée Workday : API cxs 422 vs 404 = scan de tenants (CDPQ, Beneva, PSP, BDC, Rio Tinto, Walmart 972 offres…) ; 8 NOUVEAUX ATS (17 total) : Taleo, Njoyn (anti-Radware Scrapfly), UKG, iCIMS, SuccessFactors (Bombardier 843 !), ADP, Cegid DigitalRecruiters (Santé Québec), Workland (CSSDM) — secteur public santé/éducation/municipal débloqué (Emplois Santé MTL, CIUSSS-EMTL, Gatineau, Terrebonne, STL, RTC) ; 3 portails AGGREGATORS (Jobillico, Espresso-Jobs, Guichet-Emplois — Québec Emploi écarté : clicSÉQUR) avec dédup direct-prioritaire vérifiée ; is_direct + badge « Offre directe » + POST /api/employeurs/offres (dépôt modéré) + page /employeurs ; purge hors-QC (130 offres, _NON_QC_CITIES) + fix villes McGill ; offres actives 4 442→8 400+ (direct 7 943 / portails 461). Intégré à api-ka (commit 609d6bb) : collecteur jobka_data quotidien + supervision connector_health (9 services). | 2026-08-19 : CAMPAGNE FRONT-END MOBILE (13 sites) — menus déroulants tactiles fiabilisés (verrous de scroll, backdrops portalisés hors headers backdrop-filter, hamburgers ajoutés à job-ka/trouve-ka/auto-ka), éléments du bas stabilisés (dvh, safe-area via viewport-fit=cover, échelle z-index --z-*), anti-zoom iOS ≥16 px, cibles tactiles ≥44 px, overflow-x clip html+body ; socle mobile commun dans ka-ui/tokens.css + ka-agent v2 ; audit+régression : ka-ui/mobile-audit.mjs & regression-mobile.mjs, inventaire+rapport : ka-ui/audit/. Commit poussé par app sur son nœud. Phase 3 expansion 2026-08-19 : +30 sources (réseau Atlas Workland ~80 employeurs/1 238 offres via API, plateforme carrieresante.gouv.qc.ca ×8, familles taleo_tbe + wpjobmanager, Hydro-Québec, Deloitte, CGI, Sépaq…), pagination Njoyn (Santé Montréal 50→762), réparations lassonde/workland ; 12 496 offres publiées / 12 739 actives canoniques (10 407/10 709 avant) ; cycle 246 sources 0 échec ; 71/71 tests." | |
| 231 | + }, | |
| 232 | + { | |
| 233 | + "app": "admin-ka", | |
| 234 | + "node": "M3U96a", | |
| 235 | + "port": 3300, | |
| 236 | + "domain": "www.administration-ka.com", | |
| 237 | + "process_manager": "launchd", | |
| 238 | + "label_or_name": "io.adminka.backend + io.adminka.ngrok", | |
| 239 | + "dir": "~/apps/admin-ka", | |
| 240 | + "start": "launchctl bootstrap gui/501 ~/Library/LaunchAgents/io.adminka.backend.plist ; launchctl bootstrap gui/501 ~/Library/LaunchAgents/io.adminka.ngrok.plist", | |
| 241 | + "deployed": "2026-08-19", | |
| 242 | + "notes": "Console Admin-Ka v2 (mdp app). Pilote le VRAI claude CLI (stream-json) sur le nœud de déploiement de chaque app KA (ssh + tunnel inverse pour permissions). Modèles Fable5/Opus5/Sonnet5/Haiku, modes default/acceptEdits/plan/bypass, sessions détachées+replay, monitoring Écosystème 13 sites (SQLite data/eco.sqlite, checks 45s, sweep nœuds 5min). UI identité Ka." | |
| 243 | + }, | |
| 244 | + { | |
| 245 | + "app": "house-ka", | |
| 246 | + "node": "M4M64b", | |
| 247 | + "port": 8098, | |
| 248 | + "domain": "www.house-ka.com", | |
| 249 | + "process_manager": "pm2", | |
| 250 | + "label_or_name": "house-ka-web / house-ka-sync / house-ka-ngrok", | |
| 251 | + "dir": "~/apps/house-ka", | |
| 252 | + "start": ".venv/bin/python run.py serve 8098 (+ watch 240)", | |
| 253 | + "deployed": "2026-08-27", | |
| 254 | + "notes": "Agrégateur maisons à vendre Canada hors Québec (EN, Ontario d'abord) — fork immo-ka, 202k fiches DDF RealtyPress, repo gitsrv:srv/git/house-ka.git" | |
| 255 | + }, | |
| 256 | + { | |
| 257 | + "app": "rent-ka", | |
| 258 | + "node": "M4M36", | |
| 259 | + "port": 8125, | |
| 260 | + "domain": "www.rent-ka.com", | |
| 261 | + "process_manager": "pm2", | |
| 262 | + "label_or_name": "rent-ka-web / rent-ka-sync / rent-ka-ngrok", | |
| 263 | + "dir": "~/apps/rent-ka", | |
| 264 | + "start": ".venv/bin/python run.py serve 8125 | watch 60 | ngrok http --url=www.rent-ka.com 8125", | |
| 265 | + "deployed": "2026-08-27", | |
| 266 | + "notes": "English rentals aggregator Canada ex-QC (fork lou-ka). Repo gitsrv:srv/git/rent-ka.git. DB data/rentka.db. 81 connecteurs (65 ON + 11 nationaux + 5 portails). pm2 startup launchd activé." | |
| 267 | + }, | |
| 268 | + { | |
| 269 | + "app": "uqo-eval", | |
| 270 | + "node": "M4M36", | |
| 271 | + "port": 8150, | |
| 272 | + "domain": "www.uqo-eval.app", | |
| 273 | + "process_manager": "pm2", | |
| 274 | + "label_or_name": "uqo-eval + uqo-eval-ngrok", | |
| 275 | + "dir": "~/apps/uqo-eval", | |
| 276 | + "start": "cd ~/apps/uqo-eval && PORT=8150 pm2 start /opt/homebrew/bin/npm --name uqo-eval --interpreter none -- start -- -H 0.0.0.0 -p 8150 ; pm2 start /opt/homebrew/bin/ngrok --name uqo-eval-ngrok --interpreter none -- http --url=www.uqo-eval.app 8150", | |
| 277 | + "deployed": "2026-08-28", | |
| 278 | + "notes": "UQO Éval — laboratoire pédagogique d'évaluation immobilière (cours IMM1003 · IMM1033, Simon-Pierre Boucher, UQO). Dérivé de Vrai-Prix (copie indépendante, base SQLite 2 Go clonée le 2026-08-28, aucun lien avec la prod M3U96a). Next.js 16 + better-sqlite3 ; identité UQO officielle (bleu PMS 7469, logo officiel), mode étudiant, assistante IA Éva (/api/ka local), PDF avec logo UQO + mention des cours. EN LIGNE sur https://www.uqo-eval.app (validé bout-en-bout 2026-08-28). Source laptop : ~/Desktop/uqo-eval (branche uqo-eval, sans remote). | 2026-08-28 : /stats/marche — merge base Immo-Ka (copie data/immoka.db + table uqo_eval, jumelage spatial lat/lng, 66 072 annonces évaluées + duel des méthodes coût/rôle indexé ; pipeline scripts/run-eval-shards.sh + build-marche-stats.mjs)" | |
| 279 | + }, | |
| 280 | + { | |
| 281 | + "app": "maclustr-agentd", | |
| 282 | + "node": "M4M64a", | |
| 283 | + "port": 9210, | |
| 284 | + "domain": null, | |
| 285 | + "process_manager": "launchd", | |
| 286 | + "label_or_name": "io.maclustr.agentd", | |
| 287 | + "dir": "~/maclustr-agentd", | |
| 288 | + "start": "launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/io.maclustr.agentd.plist", | |
| 289 | + "deployed": "2026-08-30", | |
| 290 | + "notes": "Backend HTTP (stdlib py, /usr/bin/python3 obligatoire cf. Local Network Privacy macOS 26) de l'app iOS MacLustr. Collecte 20 nœuds via SSH clé id_ed25519_agentd sur IP LAN 192.168.2.x (fanout, Tailscale inter-nœuds bloqué ACL). Endpoints: /health, /api/cluster, /api/history, /api/nas, /api/node/<n>/{top,ports,action}. Bearer token dans config.json (0600). Redécouverte LAN via marqueurs ~/.maclustr-node. Source: ~/Desktop/Cluster/CLUSTER_APPLE/maclustr-ios-v2/agent." | |
| 291 | + } | |
| 292 | + ], | |
| 293 | + "history_note": "2026-08-05 (2) : hilmacorp (page statique smiley, M3U96b:8070, www.hilmacorp.ai) déployé puis supprimé le même jour à la demande — pm2 delete hilmacorp + hilmacorp-ngrok, pm2 save, ~/apps/hilmacorp effacé, domaine down (404 ngrok). Source locale intacte : ~/Desktop/hilmacorp-page. | 2026-07-31 (2) : NETTOYAGE COMPLET M3U96a + M3U96b. zyquo-cloud-web migré M3U96a→M2U64:8080 (www.zyquo.cloud OK). Supprimés M3U96a : zyq-bot (www.zyq.bot, DB zyqbot sauvegardée sur laptop ~/Desktop/zyq-bot-final-backup-20260731 avec .env + anciens backups), cashout100 (www.cashout100.com), influenceursqc (www.influenceursqc.com) — pm2 vidé+tué, plist pm2 retiré, postgresql@17+redis stoppés, crontab backup retirée. Supprimés M3U96b : maclustr-llm-api (llm.maclustr.io) + chat-maclustr (chat.maclustr.io) — 3 LaunchDaemons ai.maclustr.* bootout+retirés, ~/.config/maclustr-llm-api effacé, cache HF 82 Go effacé, pm2 retiré. Domaines down : www.zyq.bot, www.cashout100.com, www.influenceursqc.com, llm.maclustr.io, chat.maclustr.io. Sources locales intactes sur laptop. | 2026-07-30 (2) : spboucher-ai supprimé de M2U64 — pm2 delete spboucher + spboucher-ngrok, pm2 save, ~/spboucher-ai et logs pm2 effacés, port 3100 libre, www.spboucher.ai down. Source locale intacte : ~/Desktop/spboucher. | 2026-07-30 : M3U96a nettoyé — llm-api (llm.maclustr.io, gateway 8080 + mlx_lm 8081) et chat-web (chat.maclustr.io, 8090) arrêtés et supprimés : 7 plists launchd io.maclustr.{chat-web,chat-web-backup,chat-ngrok,llm-gateway,llm-ngrok,llm-watchdog,mlx-backend} retirés, ~/maclustr effacé (services + backups), caches modèles ~/.cache/huggingface effacés (~35 Go : Qwen3.6-35B-A3B-8bit + Qwen3-30B-A3B-Instruct-2507-4bit). Sources locales intactes : ~/Desktop/LLM-MACLUSTR et ~/Desktop/CHAT-MACLUSTR. Conservés sur le node : nasmount, caffeinate. | 2026-07-27 : macos-app (www.macos-app.dev, M2U64:8090) supprimée — pm2 delete macos-app + macos-app-ngrok, pm2 save, ~/macosapp et logs pm2 effacés. Source locale intacte : ~/Desktop/macosapp. | 2026-07-26 : nettoyage complet M3U96a/M3U96b/M4M64a/M4M64b — supprimés : zyquo (www.zyquo.ai), llm-agent (www.llm-agent.bot), hilmacorp (www.hilmacorp.ai), chat-maclustr (chat.maclustr.io), gpt-oss-120b-api (llm.maclustr.io, modèle HF cache effacé ~63 Go). PM2 vidé, plists launchd app/ngrok retirés (io.maclustr.llm-*, pm2.*, iogpu-wired-limit), redis+postgresql@17 stoppés sur M3U96a. Conservés : caffeinate, nasmount, tailscale. Apps M2U64 pré-existantes toujours actives (hors registre) : ports 3000 (console.maclustr.io), 5000 (www.vquant.ai), 5001 (cloud.spboucher.ai), 8787 (app.maclustr.io), 8003/8004 (mlx), 5432 (postgres).\n2026-09-04 : retrait de 29 apps web du cluster (M3U96a×9, M3U96b×13, M2U64×2, M4M64a×3, M4M64b×1, M4M36×1) — chacune commitée/poussée sur spbgit (~/srv/git sur M3U96a) avec bannière README « URL live hors fonction », secrets+DB sauvegardés dans ~/cluster-cleanup-backup-20260904/<app>/ sur le nœud, PM2/launchd/ngrok arrêtés, répertoires supprimés. Entrées déplacées dans `decommissioned`. Domaines ngrok réservés non touchés. PM2 de M3U96b toujours cassé (3 démons depuis pm2 update 13:52) — dump.pm2 filtré 45→18.\n2026-09-04 (soir) : passerelle déplacée sur M1M32 (spbgit + mld) ; toutes les apps redéployées via mld (voir cluster-registry.json).", | |
| 294 | + "decommissioned": [ | |
| 295 | + { | |
| 296 | + "app": "hfmarketdata", | |
| 297 | + "node": "M4M64a", | |
| 298 | + "port": 8090, | |
| 299 | + "domain": "www.hfmarketdata.io", | |
| 300 | + "process_manager": "pm2", | |
| 301 | + "label_or_name": "hfmarketdata-api + hfmarketdata-ngrok", | |
| 302 | + "dir": "~/hfmarketdata", | |
| 303 | + "start": "pm2 start ~/hfmarketdata/run.sh --name hfmarketdata-api ; pm2 start /opt/homebrew/bin/ngrok --name hfmarketdata-ngrok --interpreter none -- http --url=www.hfmarketdata.io 8090", | |
| 304 | + "deployed": "2026-08-10", | |
| 305 | + "notes": "HF Market Data — API OPEN (sans auth) de données de marché haute fréquence + plateforme docs React (anglais). | 2026-08-10 v2: refonte thème CLAIR « éditorial sharp » (papier #f5f3ee, encre #141814, accent bleu signal #2f6bff, Space Grotesk/Inter/JetBrains Mono, bordures encre + ombres décalées dures, ticker marquee encre, héros uppercase outline+surligné, stat-chips) + docs sophistiquées: sidebar groupée GET, cartes endpoint numérotées, tableaux paramètres avec chips required/optional, blocs REQUEST (tabs curl/Python/JS + Copy) et RESPONSE (Example/Run live), accordéons « Response fields » documentant chaque colonne (17 champs options avec greeks), sections Conventions + Errors (codes HTTP) + Live status. | 2026-08-10 v3: l'utilisateur a rejeté le style brutaliste (« trop laid », pas de fonds noirs) → redesign éditorial raffiné: serif Fraunces italique pour titres/accents, fond #fbfaf7, hairlines, ombres douces, pilules, blocs de code CLAIRS (#f4f2ec), ticker marquee supprimé. + 2 nouveaux endpoints (demande utilisateur: fenêtres précises multi-tickers): GET /v1/bars/{asset}?tickers=A,B,C (multi-tickers, limit par ticker, ex. 4h de 1min sur une watchlist) et GET /v1/snapshot/{asset}?tickers=&at= (coupe transversale: dernière barre ≤ instant précis pour chaque ticker). Options reprioritisées en tête du download (priority 5, avant les barres). | 2026-08-10 v4: INGESTION TERMINÉE 745/745 (26,46 G lignes, 558 814 parquet, 350 Go, options 66/66) — 2 pièges de fin: zips options 2023 q1/q3 en Deflate64 → fallback 7zz (installé sur le node), FX intraday = date+heure en 2 colonnes séparées (yyyyMMdd,HH:mm:ss) → schéma SPLIT_DT dédié (les 4 jobs fx intraday étaient « done » à 0 lignes, resetés+refaits). + plateforme v4: PLAYGROUND interactif sur chaque endpoint (champs par paramètre, selects pour enums, adjustment dépendant de l'asset, URL live, Send request avec statut+latence, snippets curl/Python/JS régénérés, Copy URL/code), section « What you can do » (6 capacités), héros avec métriques finales. Repo: https://git.spboucher.ai/hfmarketdata. Relance download: ~/frd/run_download.sh (exporte FRD_USERID — plus hardcodé, api.txt gitignoré). FastAPI+DuckDB lit en place le lac Parquet zstd /Volumes/ssd/firstratedata/parquet (construit par ~/frd/frd_downloader.py — downloader FirstRate Data complet: options par trimestre 2010→auj., stock/etf/futures/crypto/index/fx en 1min→1day × tous ajustements, manifest de reprise state/manifest.json, zips/CSV bruts supprimés au fil de l'eau). Endpoints: /v1/status, /v1/{asset}/tickers, /v1/bars/{asset}/{ticker} (json/csv, 2M rows max csv), /v1/options/{quarters,tickers,chain,expirations,history}, Swagger /docs, site React servi à /. run.sh: uvicorn 2 workers :8090, HFMD_DATA_ROOT=/Volumes/ssd/firstratedata, HFMD_WEB_DIST=~/hfmarketdata/dist. La couverture API grandit automatiquement pendant que le download tourne (nohup, PID variable — relancer: cd ~/frd && nohup venv/bin/python frd_downloader.py --data-root /Volumes/ssd/firstratedata --workers 2 &). ATTENTION: fichiers FirstRate = fins de ligne mixtes LF/CRLF → duckdb read_csv exige strict_mode=false. Node imposé par l'utilisateur. pm2 save fait. Source laptop: ~/Desktop/firstratedata (downloader) + ~/Desktop/firstratedata/hfmarketdata (app: api/main.py + web/ Vite React, build dist rsyncé). En-tête auteur Simon-Pierre Boucher — contact@spboucher.ai partout. | 2026-08-10 fix: _rows_to_response sérialise NaN/±inf en null (les vieux trimestres options ont des Greeks NaN → 500 JSON). Committé+pushé (hfmarketdata.git).", | |
| 306 | + "decommissioned": "2026-09-04", | |
| 307 | + "spbgit_repo": "hfmarketdata (hfmarketdata/)", | |
| 308 | + "backup": "M4M64a:~/cluster-cleanup-backup-20260904/hfmarketdata/ (.env, *.db, data/)", | |
| 309 | + "status": "retirée du cluster — processus arrêtés, répertoire supprimé, README « URL live hors fonction » ; source de vérité = repo spbgit" | |
| 310 | + }, | |
| 311 | + { | |
| 312 | + "app": "draft2027", | |
| 313 | + "node": "M3U96b", | |
| 314 | + "port": 8100, | |
| 315 | + "domain": "www.draft2027.com", | |
| 316 | + "process_manager": "pm2", | |
| 317 | + "label_or_name": "draft2027 + draft2027-ngrok", | |
| 318 | + "dir": "~/apps/draft2027", | |
| 319 | + "start": "cd ~/apps/draft2027 && pm2 start /opt/homebrew/bin/node --name draft2027 --interpreter none -- server.mjs 8100 ; pm2 start /opt/homebrew/bin/ngrok --name draft2027-ngrok --interpreter none -- http --url=www.draft2027.com 8100", | |
| 320 | + "deployed": "2026-08-08", | |
| 321 | + "notes": "DraftVision 2027 — plateforme monopage de prévision du repêchage LNH 2027 (React 18 UMD + Babel standalone ÉPINGLÉ @7.26.4 — latest = Babel 8 casse le runtime JSX classique). server.mjs zéro dépendance, healthcheck /healthz. Données réelles du pipeline de recherche 2026-08-08 (voir RECHERCHE.md dans le dir): 50 espoirs, consensus pondéré 6 sources (Wheeler 30%/DFO 25%/Tankathon 20%/THN 10%/BR 10%/Button 5%), ordre simulé sur classement 2025-26, 6 picks 1re ronde échangés avec conditions dynamiques, loterie règles 2026 (top-11, max 10 rangs, VAN 25,5%). Chat Claude + bouton MàJ appellent api.anthropic.com sans clé — fonctionnels seulement dans un env. artifact claude.ai (erreur FR propre sinon). Node imposé par l'utilisateur. pm2 save fait. Source laptop: ~/Desktop/mock. En-tête auteur Simon-Pierre Boucher — contact@spboucher.ai. | 2026-08-08 v1.1: vrais logos SVG des 32 équipes (demande utilisateur, remplace les écussons stylisés du CLAUDE.md) hotlinkés depuis assets.nhle.com (variante _dark), repli automatique vers écusson stylisé si échec de chargement; mention légale au footer. | 2026-08-08/09 v2: ajustements Hlinka-Gretzky 2026 dans le consensus (bornés ±3 rangs, sommaires officiels hlinkagretzkycup.ca — Joseph malus +1,0 → Privalov 2e/Joseph 3e; England devant Sundström) ; loterie refaite sur table NOMINALE officielle 1-16 (18,5→0,5%, transfert des points 12-16 à la pire équipe, rang 12 éligible au 2e tirage — validée 300k tirages) ; +5 gardiens avec vraies stats (Déniger/Vetrov/Nikolaev/Ņikitins/Salén Forsberg) ; +Selic et L'Italien ; 21 tirs G/D complétés, nationalités résolues (Davidson/Gordon-Carroll/Walos USA, Murata CAN) ; condition TOR→PHI corrigée SANS PROTECTION (bascule loterie 2026, PuckPedia) ; Yakunin = Spartak (pas CSKA). Docs compagnons servis: ADMIN-REPECHAGE-2027.md, COULISSES-2027.txt (intel sourcé, zéro vie privée — joueurs mineurs), RECHERCHE.md.", | |
| 322 | + "decommissioned": "2026-09-04", | |
| 323 | + "spbgit_repo": "draft2027 (créé)", | |
| 324 | + "backup": "M3U96b:~/cluster-cleanup-backup-20260904/draft2027/ (.env, *.db, data/)", | |
| 325 | + "status": "retirée du cluster — processus arrêtés, répertoire supprimé, README « URL live hors fonction » ; source de vérité = repo spbgit" | |
| 326 | + }, | |
| 327 | + { | |
| 328 | + "app": "qwhpi", | |
| 329 | + "node": "M3U96a", | |
| 330 | + "port": 3000, | |
| 331 | + "domain": "www.indexqc.house", | |
| 332 | + "process_manager": "pm2", | |
| 333 | + "label_or_name": "qwhpi-api + qwhpi-web + qwhpi-ngrok", | |
| 334 | + "dir": "~/cluster-projects/qwhpi", | |
| 335 | + "start": "cd ~/cluster-projects/qwhpi/api && pm2 start \"python3 -m uvicorn app.main:app --host 127.0.0.1 --port 8080\" --name qwhpi-api ; cd ../web && pm2 start \"npm run start\" --name qwhpi-web ; pm2 start \"ngrok http 3000 --url=www.indexqc.house --log=stdout\" --name qwhpi-ngrok", | |
| 336 | + "deployed": "2026-08-06", | |
| 337 | + "notes": "QHPI — Quebec Housing Price Index (v2.1 MENSUEL depuis 2026-08-08; ex-QWHPI hebdo). Méthode robuste: RTD (fenêtres 13 mois Huber, mean splice) pour la province, time-dummy local direct pour cellules ≥40 tx/mois, Kalman hiérarchique pour cellules minces; buildingType banni (missingness backfillée par vagues régionales = marches artificielles). Canonique: data/processed/qhpi_monthly.parquet (112 séries × 67 mois, model_version 2.1.0). FastAPI :8080 (local only, champs period/monthly_pct/3m/6m/yoy) ; Next.js :3000 proxifie /v1,/docs,/health (build NEXT_PUBLIC_API_URL=\"\") ; ngrok → www.indexqc.house. Refresh: make refresh sur laptop (03→09→11) puis rsync data/processed/ (API recharge via mtime). Node imposé par l'utilisateur malgré la règle M3U96a=staging. pm2 save fait. Source laptop: ~/Desktop/QWHPI. En-tête auteur Simon-Pierre Boucher — contact@spboucher.ai partout.", | |
| 338 | + "decommissioned": "2026-09-04", | |
| 339 | + "spbgit_repo": "qwhpi", | |
| 340 | + "backup": "M3U96a:~/cluster-cleanup-backup-20260904/qwhpi/ (.env, *.db, data/)", | |
| 341 | + "status": "retirée du cluster — processus arrêtés, répertoire supprimé, README « URL live hors fonction » ; source de vérité = repo spbgit" | |
| 342 | + }, | |
| 343 | + { | |
| 344 | + "app": "hilmacorp", | |
| 345 | + "node": "M3U96a", | |
| 346 | + "port": 8070, | |
| 347 | + "domain": "www.hilmacorp.ai", | |
| 348 | + "process_manager": "pm2", | |
| 349 | + "label_or_name": "hilmacorp + hilmacorp-ngrok", | |
| 350 | + "dir": "~/apps/hilmacorp", | |
| 351 | + "start": "pm2 start ./node_modules/.bin/next --name hilmacorp --interpreter none -- start -H 0.0.0.0 -p 8070 ; pm2 start /opt/homebrew/bin/ngrok --name hilmacorp-ngrok --interpreter none -- http --url=www.hilmacorp.ai 8070", | |
| 352 | + "deployed": "2026-08-06", | |
| 353 | + "notes": "Site corporatif Hilmacorp.ai reconstruit from scratch (Next.js 15 App Router, TS strict, Tailwind v4, bilingue FR/EN via segment [locale], 7 pages, formulaire /api/contact avec validation — messages persistés dans ~/apps/hilmacorp/data/contact-messages.jsonl tant que RESEND_API_KEY absent du .env). En-tête auteur Simon-Pierre Boucher dans chaque fichier (règle CLAUDE.md projet). Node imposé par l'utilisateur malgré la règle M3U96a=staging. pm2 save fait. Source laptop: ~/Desktop/hilmacorp. Tests: npm test (8 pass). | 2026-08-06 v2: refonte majeure design + contenu — thème CLAIR éditorial (fond papier #faf9f6, cartes blanches, accent vert profond #0b6b4a, titres serif Fraunces + Inter), nouvelles sections (4 engagements accueil, principes /about, déroulement des mandats /services, cartes infos /contact). Rebuild + pm2 restart OK. | 2026-08-06 v2.1: pages légales /terms + /privacy (FR/EN, droit QC/Loi 25) + gestion cookies (bandeau consentement CookieConsent, cookie hilmacorp_cookie_consent 12 mois, bouton « Préférences de cookies » au footer + /privacy, tableau des témoins). Sitemap MAJ (18 URLs). Rebuild + pm2 restart OK. | 2026-08-06 v2.2: page /apps (nav+footer, 20 URLs sitemap) — 2 plateformes web (AI Risk Index → airiskindex.io, LLM Index → llmindex.io, liens externes) + 4 apps macOS Zyquo (Agent/Cloud/Local/Router) avec DMG servis depuis public/downloads/ (~31 Mo, sources ~/Desktop/macos/zyquo/*/dist). Rebuild + pm2 restart OK, 4 DMG vérifiés 200.", | |
| 354 | + "decommissioned": "2026-09-04", | |
| 355 | + "spbgit_repo": "hilmacorp (créé)", | |
| 356 | + "backup": "M3U96a:~/cluster-cleanup-backup-20260904/hilmacorp/ (.env, *.db, data/)", | |
| 357 | + "status": "retirée du cluster — processus arrêtés, répertoire supprimé, README « URL live hors fonction » ; source de vérité = repo spbgit" | |
| 358 | + }, | |
| 359 | + { | |
| 360 | + "app": "voyage2026", | |
| 361 | + "node": "M3U96b", | |
| 362 | + "port": 8060, | |
| 363 | + "domain": "www.voyage2026toronto.com", | |
| 364 | + "process_manager": "pm2", | |
| 365 | + "label_or_name": "voyage2026 + voyage2026-ngrok", | |
| 366 | + "dir": "~/apps/voyage2026", | |
| 367 | + "start": "pm2 start /opt/homebrew/bin/node --name voyage2026 --interpreter none -- server.mjs 8060 ; pm2 start /opt/homebrew/bin/ngrok --name voyage2026-ngrok --interpreter none -- http --url=www.voyage2026toronto.com 8060", | |
| 368 | + "deployed": "2026-08-05", | |
| 369 | + "notes": "Page statique — itinéraire road trip Québec→Toronto 17-24 août 2026 (concert Ed Sheeran LOOP Tour, Rogers Centre 20 août). server.mjs zéro dépendance, healthcheck /healthz. Design sombre v2 (countdown live, scroll reveal, nav collante). Photos hotlinkées depuis Wikimedia Commons. Source laptop: ~/Desktop/voyage. pm2 save fait.", | |
| 370 | + "decommissioned": "2026-09-04", | |
| 371 | + "spbgit_repo": "voyage2026 (créé)", | |
| 372 | + "backup": "M3U96b:~/cluster-cleanup-backup-20260904/voyage2026/ (.env, *.db, data/)", | |
| 373 | + "status": "retirée du cluster — processus arrêtés, répertoire supprimé, README « URL live hors fonction » ; source de vérité = repo spbgit" | |
| 374 | + }, | |
| 375 | + { | |
| 376 | + "app": "zyquo-cloud-web", | |
| 377 | + "node": "M2U64", | |
| 378 | + "port": 8080, | |
| 379 | + "domain": "www.zyquo.cloud", | |
| 380 | + "process_manager": "pm2", | |
| 381 | + "label_or_name": "zyquo-cloud-web + zyquo-cloud-ngrok", | |
| 382 | + "dir": "~/zyquo-cloud-web", | |
| 383 | + "start": "pm2 start server.mjs --name zyquo-cloud-web -- 8080 dist ; pm2 start ngrok --name zyquo-cloud-ngrok -- http --url=www.zyquo.cloud 8080", | |
| 384 | + "deployed": "2026-07-31", | |
| 385 | + "notes": "Zyquo Cloud Web (browser edition) — static React/Vite bundle, zero-dep server.mjs (/healthz). MIGRÉ de M3U96a vers M2U64 le 2026-07-31 (demande explicite malgré M2U64=prod). pm2 save fait. Source: laptop ~/Desktop/zyquo-cloud-web. Apex zyquo.cloud toujours sans DNS.", | |
| 386 | + "decommissioned": "2026-09-04", | |
| 387 | + "spbgit_repo": "zyquo-cloud-web", | |
| 388 | + "backup": "M2U64:~/cluster-cleanup-backup-20260904/zyquo-cloud-web/ (.env, *.db, data/)", | |
| 389 | + "status": "retirée du cluster — processus arrêtés, répertoire supprimé, README « URL live hors fonction » ; source de vérité = repo spbgit" | |
| 390 | + }, | |
| 391 | + { | |
| 392 | + "app": "immbot-ai", | |
| 393 | + "node": "M3U96b", | |
| 394 | + "port": 3070, | |
| 395 | + "domain": "www.immbot.ai", | |
| 396 | + "process_manager": "pm2", | |
| 397 | + "label_or_name": "immbot-ai + immbot-ngrok", | |
| 398 | + "dir": "~/apps/immbot-ai", | |
| 399 | + "start": "pm2 start ./node_modules/.bin/next --name immbot-ai --interpreter none -- start -p 3070 ; pm2 start ngrok --name immbot-ngrok --interpreter none -- http --url=www.immbot.ai 3070", | |
| 400 | + "deployed": "2026-08-04", | |
| 401 | + "notes": "Immbot AI — plateforme pédagogique IA pour IMM1003/IMM1033 (UQO). Next.js 15 + node:sqlite (FTS5) + embeddings locaux multilingual-e5-small + OpenRouter (clé dans ~/apps/immbot-ai/.env). Health: /api/health. Cours sources rsyncés dans ~/apps/IMM1003-20 et ~/apps/IMM1033-20 (+ ~/apps/output/course-analysis). Base: ~/apps/immbot-ai/data/immbot.db (2063 fragments, seed 129 questions/140 cartes/6 examens). Admin initial admin/admin123 avec changement forcé. ATTENTION: `pnpm build/start` plante sur ce node (bug pnpm runDepsStatusCheck) — utiliser ./node_modules/.bin/next directement. Redéploiement: immbot-ai/scripts/deploy-m3u96b.sh depuis le laptop. pm2 save fait. Source laptop: ~/Desktop/UQO/UQO_PREPARATION_COURS/immbot-ai.", | |
| 402 | + "decommissioned": "2026-09-04", | |
| 403 | + "spbgit_repo": "immbot-ai (créé)", | |
| 404 | + "backup": "M3U96b:~/cluster-cleanup-backup-20260904/immbot-ai/ (.env, *.db, data/)", | |
| 405 | + "status": "retirée du cluster — processus arrêtés, répertoire supprimé, README « URL live hors fonction » ; source de vérité = repo spbgit" | |
| 406 | + }, | |
| 407 | + { | |
| 408 | + "app": "airiskindex", | |
| 409 | + "node": "M3U96b", | |
| 410 | + "port": 3000, | |
| 411 | + "domain": "www.airiskindex.io", | |
| 412 | + "process_manager": "pm2", | |
| 413 | + "label_or_name": "airiskindex-web + airiskindex-ngrok", | |
| 414 | + "dir": "~/apps/airiskindex", | |
| 415 | + "start": "./run-web.sh (next start -p 3000) / ./run-ngrok.sh (ngrok http 3000 --url=https://www.airiskindex.io)", | |
| 416 | + "deployed": "2026-08-05", | |
| 417 | + "notes": "AI Risk Index (Next.js monorepo). Postgres@16 + Redis via brew services on node. Worker (BullMQ rater) NOT started yet. Demo dataset seeded; preview banner on site.", | |
| 418 | + "decommissioned": "2026-09-04", | |
| 419 | + "spbgit_repo": "airiskindex", | |
| 420 | + "backup": "M3U96b:~/cluster-cleanup-backup-20260904/airiskindex/ (.env, *.db, data/)", | |
| 421 | + "status": "retirée du cluster — processus arrêtés, répertoire supprimé, README « URL live hors fonction » ; source de vérité = repo spbgit" | |
| 422 | + }, | |
| 423 | + { | |
| 424 | + "app": "llmindex", | |
| 425 | + "node": "M3U96b", | |
| 426 | + "port": 3100, | |
| 427 | + "domain": "www.llmindex.io", | |
| 428 | + "process_manager": "pm2", | |
| 429 | + "label_or_name": "llmindex-web + llmindex-worker + llmindex-ngrok (+ llmindex-benchmark one-shot)", | |
| 430 | + "dir": "~/apps/llmindex", | |
| 431 | + "start": "pm2 startOrReload infra/ecosystem.config.cjs (web: Next standalone server.js PORT=3100; worker: tsx src/index.ts BullMQ; ngrok http 3100 --url=https://www.llmindex.io)", | |
| 432 | + "deployed": "2026-08-05", | |
| 433 | + "notes": "LLM Index (llmindex.io) — IRT-based LLM ranking platform. pnpm+turbo monorepo (web Next 14, worker BullMQ, psychometrics Python/numpy sous apps/psychometrics/.venv). Postgres@16 natif (DB llmindex, role llmindex, mdp dans ~/apps/llmindex/.env) + Redis brew (redis.conf réparé: loadmodule redisbloom/etc commentés le 2026-08-05). OPENROUTER_API_KEY dans .env. Seed: 338 modèles OpenRouter, run demo_seed (bannière DEMO). Redéploiement: rsync depuis laptop ~/Desktop/llmindex puis APP_DIR=~/apps/llmindex bash infra/deploy.sh. Health: /api/v1/health. ATTENTION pnpm run bugué sur ce node → utiliser binaires directs (./node_modules/.bin/next etc.). pm2 save fait. MAJ 2026-08-05: refonte frontend responsive (logo SVG + favicon app/icon.svg, LiveLeaderboard polling 5s), benchmark live: pm2 llmindex-benchmark (tsx src/cli/benchmark-run.ts --n 30 --k 2, no-autorestart) évalue les 16 modèles ranked un à la fois (6 domaines IRT), refit après chaque modèle → leaderboard se remplit graduellement; progression publique: /api/v1/benchmark/progress (Redis). | v0.2.0 (2026-08-05): 12 domaines (agentic simulé + context-load, terminal simulé, svg_design jugé 3 juges BT, vision_ocr multimodal via resvg), extraction cascade robuste, refit filtré par version, 135 modèles RANKED_MODELS, JUDGE_MODELS=gpt-5.2/opus-4.8/gemini-3.1-pro. Benchmark ~27h en cours. Page modèle = transparence totale (réponses+duels). Méthodo détaillée + refs sur /methodology.", | |
| 434 | + "decommissioned": "2026-09-04", | |
| 435 | + "spbgit_repo": "llmindex", | |
| 436 | + "backup": "M3U96b:~/cluster-cleanup-backup-20260904/llmindex/ (.env, *.db, data/)", | |
| 437 | + "status": "retirée du cluster — processus arrêtés, répertoire supprimé, README « URL live hors fonction » ; source de vérité = repo spbgit" | |
| 438 | + }, | |
| 439 | + { | |
| 440 | + "app": "coinexplorer", | |
| 441 | + "node": "M3U96b", | |
| 442 | + "port": 8090, | |
| 443 | + "domain": "www.coinexplorer.io", | |
| 444 | + "process_manager": "pm2", | |
| 445 | + "label_or_name": "coinexplorer-indexer + coinexplorer-api + coinexplorer-ngrok", | |
| 446 | + "dir": "~/apps/coinexplorer", | |
| 447 | + "start": "pm2 start .venv/bin/python --name coinexplorer-indexer -- run_indexer.py (EXPLORER_BACKFILL_DAYS=1) ; pm2 start .venv/bin/python --name coinexplorer-api -- -m uvicorn api.main:app --host 0.0.0.0 --port 8090 ; pm2 start ngrok --name coinexplorer-ngrok --interpreter none -- http --url=www.coinexplorer.io 8090", | |
| 448 | + "deployed": "2026-08-05", | |
| 449 | + "notes": "Stablecoin block explorer (free public RPCs only, aucune cle API). 23 chaines indexees (21 EVM + Tron + Solana), FastAPI + WebSocket + dashboard sur /, /docs OpenAPI, /metrics Prometheus. Stockage SQLite WAL (~/apps/coinexplorer/explorer.db) — passer a Postgres via DATABASE_URL si besoin. ATTENTION pm2+python: lancer uvicorn via .venv/bin/python -m uvicorn (le script console uvicorn est parse par node sinon). Source laptop: ~/Desktop/explorer. pm2 save fait.", | |
| 450 | + "decommissioned": "2026-09-04", | |
| 451 | + "spbgit_repo": "coinexplorer", | |
| 452 | + "backup": "M3U96b:~/cluster-cleanup-backup-20260904/coinexplorer/ (.env, *.db, data/)", | |
| 453 | + "status": "retirée du cluster — processus arrêtés, répertoire supprimé, README « URL live hors fonction » ; source de vérité = repo spbgit" | |
| 454 | + }, | |
| 455 | + { | |
| 456 | + "app": "influenceursqc", | |
| 457 | + "node": "M3U96a", | |
| 458 | + "port": 3010, | |
| 459 | + "domain": "www.influenceursqc.com", | |
| 460 | + "process_manager": "pm2", | |
| 461 | + "label_or_name": "influenceursqc + influenceursqc-ngrok", | |
| 462 | + "dir": "~/apps/influenceursqc", | |
| 463 | + "start": "PORT=3010 pm2 start /opt/homebrew/bin/node --name influenceursqc --interpreter none -- server.js ; pm2 start /opt/homebrew/bin/ngrok --name influenceursqc-ngrok --interpreter none -- http --url=www.influenceursqc.com 3010", | |
| 464 | + "deployed": "2026-08-06", | |
| 465 | + "notes": "Plateforme de classement des influenceurs du Québec (376 profils, 6 plateformes sociales). Express SSR zéro-build, thème clair moderne, routes: / (global), /plateforme/:p, /categorie/:c, /categories, /influenceur/:slug, /api/influenceurs, /health. Données embarquées: qc_influenceurs.json (copie de ~/Desktop/qc-influenceur/data/, regénérer via scripts/merge.py puis re-rsync). Node imposé par l'utilisateur malgré la règle M3U96a=staging. pm2 save fait. Source laptop: ~/Desktop/qc-influenceur/app. | 2026-08-06 v2: galerie photo par profil (gallery.json, 3732 images pour 376 profils via Firecrawl image search, grille masonry, images liées à leur source) + vrais avatars via unavatar.io (IG/TikTok/X/YT) avec repli initiales. pm2 restart OK. | 2026-08-06 v3: dossiers éditoriaux par profil (dossiers.json — 1464 faits sourcés dont 888 datés + 1053 actualités, extraits de 376 recherches web+news Firecrawl par 13 agents) : sections Parcours-chronologie (timeline datée), Faits saillants et Dans l'actualité, chaque fait/news lié à sa source. pm2 restart OK. | 2026-08-06 v4: moteur de recherche avancé /recherche (filtres plateforme/catégorie/tier/audience min, tri, recherche plein-texte incluant faits+actualités web) + valeur nette estimée en $ CAD par profil (modèle documenté sur /methodologie: taux par plateforme × multiplicateurs tier/catégorie × années actives dérivées de la chronologie, fourchette 20-45% de capitalisation) affichée sur profils et résultats de recherche. pm2 restart OK.", | |
| 466 | + "decommissioned": "2026-09-04", | |
| 467 | + "spbgit_repo": "qc-influenceurs (app/)", | |
| 468 | + "backup": "M3U96a:~/cluster-cleanup-backup-20260904/influenceursqc/ (.env, *.db, data/)", | |
| 469 | + "status": "retirée du cluster — processus arrêtés, répertoire supprimé, README « URL live hors fonction » ; source de vérité = repo spbgit" | |
| 470 | + }, | |
| 471 | + { | |
| 472 | + "app": "valoplex", | |
| 473 | + "node": "M3U96a", | |
| 474 | + "port": 8091, | |
| 475 | + "domain": "www.valoplex.com", | |
| 476 | + "process_manager": "pm2", | |
| 477 | + "label_or_name": "valoplex + valoplex-ngrok", | |
| 478 | + "dir": "~/apps/valoplex", | |
| 479 | + "start": "cd ~/apps/valoplex && pm2 start ./node_modules/.bin/next --name valoplex --interpreter none -- start -H 0.0.0.0 -p 8091 ; pm2 start /opt/homebrew/bin/ngrok --name valoplex-ngrok --interpreter none -- http --url=www.valoplex.com 8091", | |
| 480 | + "deployed": "2026-08-09", | |
| 481 | + "notes": "Petit frère de vrai-prix, spécialisé PLEX (CUBF 1000, 2+ logements — tous les plex): 393 867 plex / 1 733 744 portes / 91 060 ventes. Modèle hédonique avancé dédié (LightGBM monotone sur aire/portes/valeur rôle, features par porte, quantiles P10-P90 calibrés par méthode conforme — couverture 79,6% vérifiée ≈ cible 80%): MdAPE 13,97% (triplex 12,8%), test temporel 2026 13,8%. Moteur comparables avec ajustement PORTES (50% du prix/porte, cap ±30%) + filtre ±1 porte. Valeur totale plex QC: 337,5 G$. Source laptop: ~/Desktop/valoplex (app/ + scripts/ hedonic_plex.py, build_valoplex_db.py; modèles models/valoplex_lgb.joblib). DB SQLite 162 Mo. Même design éditorial sharp que vrai-prix, textes/menus/rapports PDF rebrandés ValoPlex. Node imposé par l'utilisateur. pm2 save fait. En-tête auteur Simon-Pierre Boucher — contact@spboucher.ai partout. | 2026-08-09 v2: (1) fix marque header (« Prix » multi-ligne échappait au sed → affichait ValoPrix). (2) MODÈLE v2 EN RATIO : cible log(prix/valeur_role) au lieu du prix brut — corrige l'écrasement des gros immeubles (tour 3100 portes au rôle 704,8M$ : estimée 5,6M$ avant → 575,8M$ après, ratio 0,82; tous les 15M$+ désormais à ratio 0,81-1,20). Monotonicité retirée (non pertinente sur ratio), conforme recalibrée (79,9%). MdAPE inchangé 13,96%. Valeur totale plex: 388,5 G$ (était 337,5 — gros multis écrasés). (3) Pondération dynamique du moteur: écart moyen de portes des comps >1,5 → modèle 75%, >3 → 85% (tour sans comparables = 100% modèle). (4) Présentation plex partout: Portrait du plex, rôle par porte, parc de plex, méthodo section « modèle en ratio », rapports PDF. Re-prédictions 6 millésimes + DB + stats regénérées. | v3: refonte fiche 100% plex — badge Duplex/Triplex/Quadruplex/…/Multi N portes rotaté lime, frise DoorsRow (une porte SVG dessinée par logement, max 10 + badge +N, ≈$/porte), section « L'économie du plex » (4 tuiles par porte: estimé/rôle/aire/terrain par porte) + benchmark vs marché local (médiane $/porte et $/m² des comps avec delta%), Prix au m² au panneau, Portrait du plex enrichi (âge, mixité commerciale, chambres locatives, emprise au sol %, bâtiment $/m², rôle/porte), $/porte dans l'historique et sur chaque comparable (badge gabarit). FIX comparables vides: état explicite « Aucun plex comparable » (transparence + section comps) au lieu du radar/tableau vides quand nCompsUsed=0 (tours hors gabarit). | v4: (1) AUDIT du ratio — vérifié: dénominateur = rôle en vigueur à la vente (98% même millésime, 93,4% égal à l'évaluation portée par l'acte); audit production ratio médian 1,005. (2) Feature anciennete_role_mois (mois entre dat_cond_mrche du rôle et la vente — LA variable d'un modèle en ratio): holdout 13,84% (vs 13,96), test 2026 13,75% (vs 14,21), couverture 80,0% pile, biais annuels resserrés (2022: 1,018→1,014). (3) Adresses de CHAQUE porte extraites de b05v_adr_unite_evaln (adresses_json en DB, cartes chips « Les portes officielles du plex » + note honnête: le rôle public ne détaille pas l'intérieur des logements). Valeur totale plex recalée: 385,0 G$. | v5: (1) PRO FORMA investisseur interactif (lib/proforma.ts + Proforma.tsx, 15 tests): loyer implicite en forme fermée RB=(V·TGA+F)/(1−vac−v), état des résultats complet (taxes ≈1,1% du rôle, assurances, entretien, gestion, déneigement, énergie communs, réserve), RNE, TGA implicite, MRB, hypothèque canadienne (composition semestrielle), cashflow/porte/mois, DSCR, cash-on-cash, droits de mutation (barème QC + barème bonifié Montréal), liquidités totales; 4 sliders live (taux/mise/amort/TGA); section 5.1 dans le rapport pro PDF. (2) MODÈLE v5 gros immeubles — fix cas Grandjean 165 portes (était 15,4M/−20%/fourchette ×4,7): shrinkage vers ancre empirique par bande hors domaine 13+ portes (w=0,55/0,35/0,20) + calibration conforme Mondrian par bande + intervalles = dispersion empirique de bande recentrée → Grandjean: rôle 19,3M → 21,2M (ratio 1,10), fourchette 11,2-31,9M. Masse intacte (MdAPE 13,84). Total plex: 400,6 G$. | v6: pro forma COMPLET (proforma.ts +soldeApresMois, capital/intérêts an 1, rendement global an 1, projection 5 ans avec appréciation paramétrable — équité, gain total, multiple sur liquidités —, loyer de point mort + marge de sécurité, taux de point mort par bissection, sensibilité taux ±1% ; 33 tests) + 5e slider appréciation + 2 cartes UI (Création de richesse 5 ans / Marges & sensibilité). PDF tous mis à niveau : rapport standard → 3 PAGES (gabarit + valeur/porte en p.1, ligne portes officielles, p.3 = pro forma 2 colonnes + liquidités sur encre + méthodo ratio/Mondrian), rapport pro 5.1 enrichi (capital an1, équité 5 ans, point mort), rapport parc = portes·$/porte par propriété. | v7: identité ORANGE (accent #ff9f45, ocre #b25f16 — l'utilisateur a rejeté le vert). README détaillé. Repo GitHub PUBLIC: https://github.com/spboucher-ai/valoplex (racine = app/ + scripts/, exclus: app/data, data/, models/). | 2026-08-09 v8: agent IA « Ka » (page /ka + API /api/ka) — Claude Haiku 4.5 en streaming SSE, 12 outils plex (chercher_plex, evaluer_plex avec économie par porte + adresses des portes, PROFORMA_INVESTISSEUR complet paramétrable — taux/mise/amort/TGA/appréciation, cashflow par porte, DSCR, liquidités tout le kit, projection 5 ans, points morts, sensibilité —, comparables avec ajust portes, indice_marche_plex, stats par gabarit, evaluer_parc, comparer_plex, chercher_plex_secteur portes/budget, estimation_manuelle, liens_rapports), prompt caching. ANTHROPIC_API_KEY dans ~/apps/valoplex/.env.local sur le node. Lien Ka dans header+footer. | v8.1: Ka renforcé — recherche approximative, outil dossier_investisseur (éval+pro forma condensé+benchmark $/porte municipal+tendance+comps+synthèse en 1 appel), tableaux HTML, fil à défilement interne, mobile refait, 12 tours/2200 tokens.", | |
| 482 | + "decommissioned": "2026-09-04", | |
| 483 | + "spbgit_repo": "valoplex (app/)", | |
| 484 | + "backup": "M3U96a:~/cluster-cleanup-backup-20260904/valoplex/ (.env, *.db, data/)", | |
| 485 | + "status": "retirée du cluster — processus arrêtés, répertoire supprimé, README « URL live hors fonction » ; source de vérité = repo spbgit" | |
| 486 | + }, | |
| 487 | + { | |
| 488 | + "app": "earth-now", | |
| 489 | + "node": "M3U96b", | |
| 490 | + "port": 8080, | |
| 491 | + "domain": "www.earth-now.co", | |
| 492 | + "process_manager": "pm2", | |
| 493 | + "label_or_name": "earthnow-api + earthnow-web + earthnow-proxy + earthnow-ngrok", | |
| 494 | + "dir": "~/apps/earth-now", | |
| 495 | + "start": "cd ~/apps/earth-now && PORT=4000 pm2 start /opt/homebrew/bin/node --name earthnow-api --interpreter none -- apps/api/dist/server.js ; cd apps/web && API_URL=http://127.0.0.1:4000 pm2 start ./node_modules/.bin/next --name earthnow-web --interpreter none -- start -H 127.0.0.1 -p 4100 ; cd ../.. ; PROXY_PORT=8080 API_PORT=4000 WEB_PORT=4100 pm2 start /opt/homebrew/bin/node --name earthnow-proxy --interpreter none -- infra/proxy.mjs ; pm2 start /opt/homebrew/bin/ngrok --name earthnow-ngrok --interpreter none -- http --url=www.earth-now.co 8080", | |
| 496 | + "deployed": "2026-08-09", | |
| 497 | + "notes": "earth-now.co — dashboard planétaire temps réel (successeur robuste de Worldometer). Monorepo pnpm/Turborepo (~/Desktop/earth-now, git local) : packages/counter (contrat CounterModel + runtime pur value(model,t)), packages/models (PCHIP/Fourier/Holt-Winters/Kalman + garde-fous), packages/registry (40 métriques YAML + fixtures, tous les 🥇 MVP + top-10 pays), apps/api (Fastify :4000 — REST/SSE/badges SVG + pollers RT USGS & open-notify), apps/web (Next 14 :4100 — thème CLAIR défaut, layout live-first: héros population, grille « En direct » triée par vitesse de tick calculée, strip session, classement 10 pays, bande référence), infra/proxy.mjs (:8080 zéro-dép route /api|/v1|/sse|/badge→4000, reste→4100). Redéploiement: pnpm deploy:m3u96b depuis le laptop (rsync + build sur nœud + PM2 + health checks obligatoires /api/health + /sse/health≥10s). Build web avec NEXT_PUBLIC_API_URL='' (fetchs same-origin). v0.1 sans Postgres/Redis (modèles fit en mémoire depuis le registre; compose Docker prêt dans infra/ pour la suite). SSE: /sse/health paddé 2Ko (ngrok bufferise les mini-chunks). 139 tests verts. pm2 save fait. En-tête auteur Simon-Pierre Boucher partout.", | |
| 498 | + "decommissioned": "2026-09-04", | |
| 499 | + "spbgit_repo": "earth-now", | |
| 500 | + "backup": "M3U96b:~/cluster-cleanup-backup-20260904/earth-now/ (.env, *.db, data/)", | |
| 501 | + "status": "retirée du cluster — processus arrêtés, répertoire supprimé, README « URL live hors fonction » ; source de vérité = repo spbgit" | |
| 502 | + }, | |
| 503 | + { | |
| 504 | + "app": "khaelor-sh", | |
| 505 | + "node": "M3U96b", | |
| 506 | + "port": 8790, | |
| 507 | + "domain": "www.khaelor.sh", | |
| 508 | + "process_manager": "pm2", | |
| 509 | + "label_or_name": "khaelor-sh + khaelor-sh-ngrok", | |
| 510 | + "dir": "~/apps/khaelor-sh", | |
| 511 | + "start": "cd ~/apps/khaelor-sh && PORT=8790 pm2 start /opt/homebrew/bin/node --name khaelor-sh --interpreter none -- server.mjs ; pm2 start /opt/homebrew/bin/ngrok --name khaelor-sh-ngrok --interpreter none -- http --url=www.khaelor.sh 8790", | |
| 512 | + "deployed": "2026-08-09", | |
| 513 | + "notes": "KHAELOR — site de documentation + distribution du CLI khaelor (agent terminal autonome Anthropic-only, TypeScript/Node>=22). server.mjs zero-dep, healthcheck /health, 9 pages docs (dark/light), install.sh one-liner (curl -fsSL https://www.khaelor.sh/install.sh | sh) qui installe via npm depuis le tarball servi A LA RACINE du site (https://www.khaelor.sh/khaelor.tgz — PAS de GitHub, demande explicite). Tarball = npm pack du repo laptop ~/Desktop/khaelor (v0.1.0, dist/ + README, 287 fichiers, zero secret). MAJ du tarball: npm run build && npm pack dans le repo puis scp khaelor-X.Y.Z.tgz M3U96b:apps/khaelor-sh/artifacts/khaelor.tgz (server no-cache sur tarball+install.sh). Node impose par l'utilisateur. pm2 save fait. Install one-liner teste avec succes depuis M4M36 (2026-08-09). En-tetes auteur Simon-Pierre Boucher — contact@spboucher.ai partout.", | |
| 514 | + "decommissioned": "2026-09-04", | |
| 515 | + "spbgit_repo": "khaelor (website/)", | |
| 516 | + "backup": "M3U96b:~/cluster-cleanup-backup-20260904/khaelor-sh/ (.env, *.db, data/)", | |
| 517 | + "status": "retirée du cluster — processus arrêtés, répertoire supprimé, README « URL live hors fonction » ; source de vérité = repo spbgit" | |
| 518 | + }, | |
| 519 | + { | |
| 520 | + "app": "spbdrive", | |
| 521 | + "node": "M3U96b", | |
| 522 | + "port": 7430, | |
| 523 | + "domain": "drive.spboucher.ai", | |
| 524 | + "process_manager": "pm2", | |
| 525 | + "label_or_name": "spbdrive-server + spbdrive-tunnel", | |
| 526 | + "dir": "~/srv/drive/app", | |
| 527 | + "start": "pm2 startOrReload ~/srv/drive/app/deploy/ecosystem.config.cjs", | |
| 528 | + "deployed": "2026-08-10", | |
| 529 | + "notes": "SPB Drive — personal cloud (Fastify+SQLite CAS blobs, data ~/srv/drive, nightly backup cron 02:30, source ~/Desktop/drive) | v2.0.0 deployed 2026-08-10: versioning, editor, file requests (/r/<token>), storage insights", | |
| 530 | + "decommissioned": "2026-09-04", | |
| 531 | + "spbgit_repo": "drive", | |
| 532 | + "backup": "M3U96b:~/cluster-cleanup-backup-20260904/spbdrive/ (.env, *.db, data/)", | |
| 533 | + "status": "retirée du cluster — processus arrêtés, répertoire supprimé, README « URL live hors fonction » ; source de vérité = repo spbgit" | |
| 534 | + }, | |
| 535 | + { | |
| 536 | + "app": "hfchart", | |
| 537 | + "node": "M4M64b", | |
| 538 | + "port": 8130, | |
| 539 | + "domain": "www.hfchart.io", | |
| 540 | + "process_manager": "pm2", | |
| 541 | + "label_or_name": "hfchart-web + hfchart-ngrok", | |
| 542 | + "dir": "~/hfchart", | |
| 543 | + "start": "pm2 start /opt/homebrew/bin/python3 --name hfchart-web -- ~/hfchart/server.py 8130 ; pm2 start ~/bin/ngrok --name hfchart-ngrok --interpreter none -- http --url=www.hfchart.io 8130", | |
| 544 | + "deployed": "2026-08-10", | |
| 545 | + "notes": "HFChart — plateforme-catalogue de charts haute fréquence, rendu canvas 100% from scratch (zéro lib de charts). 14 types (chandelles, creuses, OHLC, ligne/escalier/aire/baseline, Heikin-Ashi, Renko, Three-Line Break, Range Bars, Kagi, Point & Figure, Comparaison % avec rebasage sur 1re barre visible) + indicateurs SMA/EMA/Bollinger/VWAP session/profil volume/RSI/MACD/ATR, thème CLAIR par défaut + sombre, schéma hausse/baisse vert-rouge ou bleu-rouge (CVD-validé), vue table, galerie live, vues partageables par URL (?group=&symbol=&tf=&type=&inds=&compare=&theme=&panel=gallery). DONNÉES: API https://www.hfmarketdata.io interrogée directement par le navigateur (CORS ouvert) — timeframes dérivés client (1h→4h, 1J→1S, 5min→15m) et boutons TF auto-activés via /v1/status quand le lac se remplit. server.py = hôte statique stdlib pur. Source laptop: ~/Desktop/hfchart (server.py + web/). ATTENTION M4M64b: (1) les binaires cask Homebrew fraîchement installés pendent à l'exec via SSH (Gatekeeper/syspolicyd, RSS 32Ko) — le ngrok utilisé est ~/bin/ngrok copié depuis M4M64a (3.39.5), PAS /opt/homebrew/bin/ngrok; (2) curl du nœud timeout même sur 127.0.0.1 (python urllib OK) — healthcheck local via python3. En-tête auteur Simon-Pierre Boucher — contact@spboucher.ai partout. pm2 save fait. Node imposé par l'utilisateur. | 2026-08-10 v2: (1) responsive smartphone — sidebar en drawer (hamburger ☰ + backdrop), topbar compacte scrollable, pinch-zoom tactile 2 doigts sur canvas (touch-action:none), largeur d'axe des prix dynamique (mesurée sur le dernier prix); (2) bouton « Exporter » : snippet HTML autonome avec données intégrées (barres visibles ≤3000, transforms/indicateurs/réglages inclus, graphe interactif sans réseau) via /embed.js (bundle concaténé transforms+indicators+renderers+engine+embed-boot, cache mtime, CORS *) + variante iframe live (?embed=1 masque topbar/sidebar/footer); (3) passe qualité rendu: filigrane symbole·tf, marge droite 6%, ombres sur chips, titres de panes (Volume/RSI/MACD/ATR), grille verticale atténuée, volume en dégradé, graduations d'axe temps, légende blur/backdrop-filter, priorité année>mois sur les ticks temps. NOTE headless-test: Chrome headless a une largeur min de fenêtre 500px — tester le mobile via une page hôte avec iframe 390px. | 2026-08-10 v3: Options Lab (options.html) — chaîne d'options historique datée (2010→auj., calls/puts côte à côte, filtres |Δ| / ± spot, ITM ombré, lazy par échéance + cache LRU), builder multi-jambes (clic bid/mid/ask → acheter/vendre) + presets (covered call, CSP, verticals, iron condor, straddle, strangle), payoff canvas (échéance + T+0 Taylor bornée ±6% du spot), breakevens/max exacts (module PUR options-math.js, 10 tests node --test), replay mark-to-market RÉEL jour par jour via /v1/options/history. Spot en UNADJUSTED pour stock/etf (strikes as-traded !). Vues partageables: options.html?ticker=&date=&expiry=&preset=&replay=1. Fix API hfmarketdata déployé sur M4M64a en même temps: NaN Greeks → null (500 sur /v1/options/history).", | |
| 546 | + "decommissioned": "2026-09-04", | |
| 547 | + "spbgit_repo": "hfchart", | |
| 548 | + "backup": "M4M64b:~/cluster-cleanup-backup-20260904/hfchart/ (.env, *.db, data/)", | |
| 549 | + "status": "retirée du cluster — processus arrêtés, répertoire supprimé, README « URL live hors fonction » ; source de vérité = repo spbgit" | |
| 550 | + }, | |
| 551 | + { | |
| 552 | + "app": "tendril", | |
| 553 | + "node": "M3U96a", | |
| 554 | + "port": 8092, | |
| 555 | + "domain": "www.ten-dril.com", | |
| 556 | + "process_manager": "pm2", | |
| 557 | + "label_or_name": "tendril-api + tendril-ngrok", | |
| 558 | + "dir": "~/apps/tendril", | |
| 559 | + "start": "cd ~/apps/tendril && TENDRIL_DIR=$HOME/apps/tendril TENDRIL_PORT=8092 pm2 startOrReload deploy/ecosystem.config.cjs --update-env", | |
| 560 | + "deployed": "2026-08-10", | |
| 561 | + "notes": "Tendril — plateforme d ingestion web (scrape/crawl/map/search) macOS Apple Silicon, alternative Firecrawl auto-hébergée. Monorepo pnpm/TS strict (packages: shared/router/egress/fetcher-http/extract/frontier + apps/api Fastify). Phase 1+ LIVRÉE (86 tests): Tier 0 undici (ordre en-têtes Safari, redirections manuelles + re-validation SSRF par saut, épinglage IP anti-rebinding, gzip/br), extraction DÉTERMINISTE (JSON-LD/OG/Twitter, Readability+fallback densité, règles Turndown GFM), SSRF (resolve-then-validate §16.5), taxonomie erreurs §15 source unique, respect robots.txt (RFC9309), endpoints POST /v1/scrape, POST /v1/map (sitemaps+robots+homepage+llms.txt), GET /v1/status, /healthz + Playground UI interactif & docs servis à / (@fastify/static, page auto-suffisante: hero thème botanique, playground scrape/map/status avec preview markdown/liens/snippets curl-python-js, référence API + table erreurs §15). API bind 127.0.0.1:8092 (loopback), ngrok = seule ingress. Tiers 1 (WKWebView Swift) & 2 (Safari) PAS ENCORE construits (nécessitent session GUI) → pages exigeant escalade échouent en ERR_TARGET_BLOCKED sous tier:auto (forcer tier:http pour sortie Tier 0). ⚠️ BLOQUANT ngrok: réserver www.ten-dril.com dans dashboard.ngrok.com/domains/new (CNAME DNS déjà en place → ngrok-cname; tendril-ngrok en restart-loop ERR_NGROK_319 jusqu à réservation, se connectera seul ensuite). ⚠️ Ce node ne résout PAS example.com (Tailscale MagicDNS split-DNS) — utiliser de vrais domaines (iana.org, etc.) pour tester. Déploiement: scripts/deploy.sh (rsync+install+build+pm2+health). Repo: https://git.spboucher.ai/tendril. Source laptop: ~/Desktop/tren-dril. En-tête auteur Simon-Pierre Boucher — contact@spboucher.ai sur chaque fichier de code. Node M3U96a imposé par le CLAUDE.md du projet (§16 production host) malgré règle staging. pm2 save fait.", | |
| 562 | + "decommissioned": "2026-09-04", | |
| 563 | + "spbgit_repo": "tendril", | |
| 564 | + "backup": "M3U96a:~/cluster-cleanup-backup-20260904/tendril/ (.env, *.db, data/)", | |
| 565 | + "status": "retirée du cluster — processus arrêtés, répertoire supprimé, README « URL live hors fonction » ; source de vérité = repo spbgit" | |
| 566 | + }, | |
| 567 | + { | |
| 568 | + "app": "svgarden", | |
| 569 | + "node": "M3U96b", | |
| 570 | + "port": 4321, | |
| 571 | + "domain": "www.svgarden.dev", | |
| 572 | + "process_manager": "pm2", | |
| 573 | + "label_or_name": "svgarden-web + svgarden-tunnel", | |
| 574 | + "dir": "~/apps/svgarden", | |
| 575 | + "start": "cd ~/apps/svgarden && pm2 startOrRestart ecosystem.config.cjs && pm2 save", | |
| 576 | + "deployed": "2026-08-10", | |
| 577 | + "notes": "SVGarden — banque searchable d'animations SVG+CSS (24 snippets seed, 7 catégories), Astro statique + server.mjs Express (:4321, /healthz = build timestamp + snippet count). Tunnel ngrok nommé 'svgarden' défini dans ~/Library/Application Support/ngrok/ngrok.yml du node (ngrok start svgarden --log=stdout sous pm2). Node imposé par le CLAUDE.md du projet. Redéploiement: ./scripts/deploy.sh depuis ~/Desktop/svgarden (laptop) — rsync (pas encore de remote git), npm ci, npm run build (validation snippets incluse, FAIL si header/frontmatter/scoping invalide), pm2 startOrRestart, healthchecks local+public. Lighthouse 100/100 perf+a11y (home et détail). Source laptop: ~/Desktop/svgarden. En-tête auteur Simon-Pierre Boucher obligatoire dans CHAQUE fichier (règle projet). pm2 save fait. | 2026-08-10 phase 2: +50 snippets « next-level » (74 total, 14 catégories — nouvelles: buttons, filters, scroll, motion-path, text-fx, charts, interactive) via 8 sous-agents parallèles; limite passée à 150 lignes/snippet (directive utilisateur, remplace le ~120 du CLAUDE.md); champ frontmatter optionnel `support` (caveat navigateur, exigé si @supports présent) rendu sur la page détail; badge « New » (≤14 jours, calculé au build); facette technique (top 18) sur la galerie; fix v1 card-lift-border (non-scaling-stroke + pathLength cassait le dash en espace écran). Lighthouse 100/100 maintenu. Redéployé, healthz=74.", | |
| 578 | + "decommissioned": "2026-09-04", | |
| 579 | + "spbgit_repo": "svgarden", | |
| 580 | + "backup": "M3U96b:~/cluster-cleanup-backup-20260904/svgarden/ (.env, *.db, data/)", | |
| 581 | + "status": "retirée du cluster — processus arrêtés, répertoire supprimé, README « URL live hors fonction » ; source de vérité = repo spbgit" | |
| 582 | + }, | |
| 583 | + { | |
| 584 | + "app": "localvm-web", | |
| 585 | + "node": "M3U96b", | |
| 586 | + "port": 8120, | |
| 587 | + "domain": "www.localvm.dev", | |
| 588 | + "process_manager": "pm2", | |
| 589 | + "label_or_name": "localvm-web + localvm-web-ngrok", | |
| 590 | + "dir": "~/localvm-web", | |
| 591 | + "start": "PORT=8120 pm2 start server.js --name localvm-web ; pm2 start /opt/homebrew/bin/ngrok --name localvm-web-ngrok -- http --url=www.localvm.dev 8120", | |
| 592 | + "deployed": "2026-08-12", | |
| 593 | + "notes": "Research showcase for ~/Desktop/localvm-research (English, light theme, mobile menu, public comments). Update: run web/sync-content.sh, then rsync -az --delete --exclude node_modules --exclude data web/ M3U96b:~/localvm-web/ && pm2 restart localvm-web. IMPORTANT: data/ holds comments.json — never delete.", | |
| 594 | + "decommissioned": "2026-09-04", | |
| 595 | + "spbgit_repo": "localvm-research (web/)", | |
| 596 | + "backup": "M3U96b:~/cluster-cleanup-backup-20260904/localvm-web/ (.env, *.db, data/)", | |
| 597 | + "status": "retirée du cluster — processus arrêtés, répertoire supprimé, README « URL live hors fonction » ; source de vérité = repo spbgit" | |
| 598 | + }, | |
| 599 | + { | |
| 600 | + "app": "modelmap-web", | |
| 601 | + "node": "M3U96b", | |
| 602 | + "port": 8140, | |
| 603 | + "domain": "www.modelmap.io", | |
| 604 | + "process_manager": "pm2", | |
| 605 | + "label_or_name": "modelmap-web + modelmap-web-ngrok", | |
| 606 | + "dir": "~/modelmap-web", | |
| 607 | + "start": "PORT=8140 pm2 start server.js --name modelmap-web ; pm2 start /opt/homebrew/bin/ngrok --name modelmap-web-ngrok --interpreter none -- http --url=www.modelmap.io 8140", | |
| 608 | + "deployed": "2026-08-12", | |
| 609 | + "notes": "modelmap.io — living research atlas for ~/Desktop/modelmap (internal cartography of local LLMs). English, light theme, violet accent; same Express architecture as localvm-web (sister project, www.localvm.dev): server renders the content/ snapshot of the research repo. Pages: Home (phases 1-11 + confidence ladder L0-L3), Research, Experiments (micro A-H), Atlas (confidence-labeled map entries), Results, Code, About. Update: run site/sync-content.sh in the repo, rsync site/ (exclude node_modules) to M3U96b:~/modelmap-web/, pm2 restart modelmap-web. Apex modelmap.io has no DNS record — only www. is routed via ngrok. | 2026-08-12 v2: refonte éditoriale (Fraunces/Inter/JetBrains Mono, logo SVG contour-lines = favicon, hero gradient, footer 4 colonnes), responsive mobile complet (menu hamburger <880px, breakpoints 560px), page /comments (formulaire visiteurs, honeypot + rate-limit/IP, store persistant HORS du dossier app: ~/.modelmap-web/comments.json sur M3U96b — survit aux rsync --delete). | 2026-08-12 v3: cartes SVG live (probes réel-vs-jumeau, bande causale, stockage warm/cold, overhead capture — palette validée dataviz) + onglet /publications (rapports techniques officiels, figures injectées {{fig:*}}, bloc citation) — TR-2026-01 publié. Miroir git: https://git.spboucher.ai/modelmap (push SSH M3U96a:srv/git/modelmap.git).", | |
| 610 | + "decommissioned": "2026-09-04", | |
| 611 | + "spbgit_repo": "modelmap (site/)", | |
| 612 | + "backup": "M3U96b:~/cluster-cleanup-backup-20260904/modelmap-web/ (.env, *.db, data/)", | |
| 613 | + "status": "retirée du cluster — processus arrêtés, répertoire supprimé, README « URL live hors fonction » ; source de vérité = repo spbgit" | |
| 614 | + }, | |
| 615 | + { | |
| 616 | + "app": "anomaly-atlas", | |
| 617 | + "node": "M3U96b", | |
| 618 | + "port": 8150, | |
| 619 | + "domain": "www.anomaly-atlas.io", | |
| 620 | + "process_manager": "pm2", | |
| 621 | + "label_or_name": "anomaly-atlas + anomaly-atlas-ngrok", | |
| 622 | + "dir": "~/apps/anomaly-atlas", | |
| 623 | + "start": "cd ~/apps/anomaly-atlas && pm2 start /opt/homebrew/bin/node --name anomaly-atlas --interpreter none -- server.js ; pm2 start /opt/homebrew/bin/ngrok --name anomaly-atlas-ngrok --interpreter none -- http --url=www.anomaly-atlas.io 8150", | |
| 624 | + "deployed": "2026-08-12", | |
| 625 | + "notes": "Plateforme publique du projet de recherche anomaly-atlas (découverte+validation rigoureuse d'anomalies statistiques dans les données HF ouvertes de hfmarketdata.io — source unique). Node imposé par l'utilisateur. Node/Express (comme localvm-web): server.js lit un snapshot du repo dans content/ (généré par web/sync-content.sh sur le laptop, source: ~/Desktop/anomaly-atlas). Pages: Home (doctrine d'honnêteté §2.1 en bannière sur CHAQUE page, taxonomie de confiance Level 0-3, phases), Atlas (findings atlas/<id>/<ver>/finding.json — vide tant que rien n'atteint Level>=1, état vide honnête), Research (docs markdown + LOG timeline), Experiments (expA-H + candidats), Results, Code, About. /health OK. PORT par défaut 8150 dans server.js. Redéploiement: ./web/sync-content.sh puis rsync -a --delete --exclude node_modules web/ M3U96b:apps/anomaly-atlas/ puis pm2 restart anomaly-atlas. pm2 save fait. AUCUN conseil financier — bannière « not investment advice » partout.", | |
| 626 | + "decommissioned": "2026-09-04", | |
| 627 | + "spbgit_repo": "anomaly-atlas (web/)", | |
| 628 | + "backup": "M3U96b:~/cluster-cleanup-backup-20260904/anomaly-atlas/ (.env, *.db, data/)", | |
| 629 | + "status": "retirée du cluster — processus arrêtés, répertoire supprimé, README « URL live hors fonction » ; source de vérité = repo spbgit" | |
| 630 | + }, | |
| 631 | + { | |
| 632 | + "app": "search-box", | |
| 633 | + "node": "M3U96a", | |
| 634 | + "port": 3020, | |
| 635 | + "domain": "www.search-box.ai", | |
| 636 | + "process_manager": "pm2", | |
| 637 | + "label_or_name": "search-box-web / search-box-ngrok", | |
| 638 | + "dir": "~/apps/search-box", | |
| 639 | + "start": "PORT=3020 pm2 start pnpm --name search-box-web -- start (cwd ~/apps/search-box); pm2 start /opt/homebrew/bin/ngrok --name search-box-ngrok -- http --url=www.search-box.ai 3020", | |
| 640 | + "deployed": "2026-08-12", | |
| 641 | + "notes": "Agentic research engine (Next.js 15 + Claude Opus 5 + Firecrawl). PostgreSQL 17 local db 'searchbox' (brew services postgresql@17). Secrets in ~/apps/search-box/.env.", | |
| 642 | + "decommissioned": "2026-09-04", | |
| 643 | + "spbgit_repo": "search-box", | |
| 644 | + "backup": "M3U96a:~/cluster-cleanup-backup-20260904/search-box/ (.env, *.db, data/)", | |
| 645 | + "status": "retirée du cluster — processus arrêtés, répertoire supprimé, README « URL live hors fonction » ; source de vérité = repo spbgit" | |
| 646 | + }, | |
| 647 | + { | |
| 648 | + "app": "worthdoing", | |
| 649 | + "node": "M3U96a", | |
| 650 | + "port": 3001, | |
| 651 | + "domain": "www.worthdoing.ai", | |
| 652 | + "process_manager": "pm2", | |
| 653 | + "label_or_name": "worthdoing-web + worthdoing-ngrok", | |
| 654 | + "dir": "~/apps/worthdoing", | |
| 655 | + "start": "pnpm start (next start -p 3001); ngrok http --url=www.worthdoing.ai 3001", | |
| 656 | + "deployed": "2026-08-12", | |
| 657 | + "notes": "WorthDoing.ai — autonomous investigation agent (Claude Opus 5 + Firecrawl). PostgreSQL db 'worthdoing' local on node. Repo: https://git.spboucher.ai/worthdoing", | |
| 658 | + "decommissioned": "2026-09-04", | |
| 659 | + "spbgit_repo": "worthdoing", | |
| 660 | + "backup": "M3U96a:~/cluster-cleanup-backup-20260904/worthdoing/ (.env, *.db, data/)", | |
| 661 | + "status": "retirée du cluster — processus arrêtés, répertoire supprimé, README « URL live hors fonction » ; source de vérité = repo spbgit" | |
| 662 | + }, | |
| 663 | + { | |
| 664 | + "app": "chat-spboucher", | |
| 665 | + "node": "M4M64a", | |
| 666 | + "port": 3000, | |
| 667 | + "domain": "chat.spboucher.ai", | |
| 668 | + "process_manager": "launchd", | |
| 669 | + "label_or_name": "ai.spboucher.chat.app + ai.spboucher.chat.ngrok + ai.spboucher.chat.backup", | |
| 670 | + "dir": "~/apps/chat.spboucher.ai", | |
| 671 | + "start": "launchctl load ~/Library/LaunchAgents/ai.spboucher.chat.{app,ngrok}.plist", | |
| 672 | + "deployed": "2026-08-12", | |
| 673 | + "notes": "Universal chat UI over OpenRouter (409 models). Next.js 16 + SQLite. Auth: user spboucher. Nightly DB backup 03:30 via launchd. Source: ~/Desktop/chat-spboucher on laptop.", | |
| 674 | + "decommissioned": "2026-09-04", | |
| 675 | + "spbgit_repo": "chat-spboucher", | |
| 676 | + "backup": "M4M64a:~/cluster-cleanup-backup-20260904/chat-spboucher/ (.env, *.db, data/)", | |
| 677 | + "status": "retirée du cluster — processus arrêtés, répertoire supprimé, README « URL live hors fonction » ; source de vérité = repo spbgit" | |
| 678 | + }, | |
| 679 | + { | |
| 680 | + "app": "forma-ka", | |
| 681 | + "node": "M3U96a", | |
| 682 | + "port": 8110, | |
| 683 | + "domain": "www.forma-ka.com", | |
| 684 | + "process_manager": "pm2", | |
| 685 | + "label_or_name": "forma-ka + forma-ka-sync + forma-ka-ngrok", | |
| 686 | + "dir": "~/apps/forma-ka", | |
| 687 | + "start": "cd ~/apps/forma-ka && pm2 start \".venv/bin/python run.py serve 8110\" --name forma-ka ; pm2 start \".venv/bin/python run.py watch 360\" --name forma-ka-sync ; pm2 start /opt/homebrew/bin/ngrok --name forma-ka-ngrok --interpreter none -- http --url=www.forma-ka.com 8110", | |
| 688 | + "deployed": "2026-08-12", | |
| 689 | + "notes": "Forma-Ka — agrégateur québécois de formations (mécanisme lou-ka adapté : connecteurs → schéma Formation riche en détails avec prix OPTIONNEL → diff SQLite hash+grâce → FastAPI /api/formations → React 18 Vite PWA thème clair accent ambré #ffd54d). Backends de fetch : requests direct → Scrapfly (asp/render_js, SCRAPFLY_API_KEY) → Firecrawl (FIRECRAWL_API_KEY) en repli auto ; clés dans ~/apps/forma-ka/.env. Fiches détail en cache BD avec clé hebdo ISO. forma-ka-sync = run.py watch 360 (sync toutes les 6 h). Node imposé par l'utilisateur malgré la règle M3U96a=staging. pm2 save fait. Source laptop : ~/Desktop/forma-ka. En-tête auteur Simon-Pierre Boucher — contact@spboucher.ai partout. Connecteurs livrés progressivement par agents (ETS Formation 297 fiches + universitaire/pro/collégial/séminaires en cours) — re-rsync + pm2 restart forma-ka forma-ka-sync après chaque vague.", | |
| 690 | + "decommissioned": "2026-09-04", | |
| 691 | + "spbgit_repo": "forma-ka", | |
| 692 | + "backup": "M3U96a:~/cluster-cleanup-backup-20260904/forma-ka/ (.env, *.db, data/)", | |
| 693 | + "status": "retirée du cluster — processus arrêtés, répertoire supprimé, README « URL live hors fonction » ; source de vérité = repo spbgit" | |
| 694 | + }, | |
| 695 | + { | |
| 696 | + "app": "ora-ka", | |
| 697 | + "node": "M4M64a", | |
| 698 | + "port": 8200, | |
| 699 | + "domain": "www.ora-ka.com", | |
| 700 | + "process_manager": "pm2", | |
| 701 | + "label_or_name": "ora-ka-web + ora-ka-sync + ora-ka-ngrok", | |
| 702 | + "dir": "~/apps/ora-ka", | |
| 703 | + "start": "pm2 start .venv/bin/python --name ora-ka-web --interpreter none --cwd ~/apps/ora-ka -- run.py serve 8200 ; pm2 start .venv/bin/python --name ora-ka-sync --interpreter none --cwd ~/apps/ora-ka -- run.py watch ; pm2 start /opt/homebrew/bin/ngrok --name ora-ka-ngrok --interpreter none -- http --url=www.ora-ka.com 8200", | |
| 704 | + "deployed": "2026-08-13", | |
| 705 | + "notes": "ORA-KA — plateforme unifiée des 5 agrégateurs Ka (~362k inscriptions). Les 5 apps reproduites FIDÈLEMENT (backends non modifiés) et montées sous préfixes: /immo (Immo-Ka, source ~/Desktop/agent-courtage), /lou (Lou-Ka, ~/Desktop/lou-ka), /fabri (Fabri-Ka, ~/Desktop/qc-store), /auto (Auto-Ka, ~/Desktop/auto), /food (Food-Ka, ~/Desktop/food-ka). Arborescence: apps/<alias>/{<pkg>/, data/, frontend/} — chaque app garde data/ et frontend/dist frères du package (chemins __file__ intacts). Frontends React/Vite rebuildés avec base '/<alias>/' + basename react-router + API_BASE (seuls changements). Landing / = BARRE DE RECHERCHE unifiée ultra-épurée (liseré conic-gradient animé 5 couleurs, chips de portée, grille mixte avec pastille d'univers) + /api/search HYBRIDE: exact (LIKE/FTS5 dans les 5 SQLite, entrelacé par univers) puis SÉMANTIQUE (embeddings OpenAI text-embedding-3-small 256d float16, index npz data/semantic/<app>.npz, brute-force numpy, seuil 0.32, hot-reload sur mtime). OPENAI_API_KEY + FIRECRAWL + SCRAPFLY(_KEY et _API_KEY) dans .env. Rebuild index: run.py index [app...] (incrémental par uid — relancer après grosses syncs). run.py watch = 5 threads aux cadences natives (immo 180min, lou 60 incl. geocode/poi/quartier, auto 120, fabri/food 360). ora-ka-sync et les apps individuelles (immo-ka M4M64a, lou-ka M3U96b, auto-ka/food-ka M4M64b, fabri-ka M4M64a) scrapent les MÊMES sources en parallèle — doublon de charge assumé. Source laptop: ~/Desktop/ora-ka. pm2 save fait. En-tête auteur Simon-Pierre Boucher — contact@spboucher.ai. | 2026-08-13: LOGIN KA ID au niveau plateforme — oraka/auth.py (JWT HS256 stdlib aud=ora-ka, users SQLite data/oraka-users.db keyed ka_id, cookie oraka_session path=/ valide sur tout le domaine incl. /immo//lou/…), router inclus avant les montages (vérifié: /api/auth servi par la racine), landing.py: pilule noire « KA ID » en topbar fixe (visible mobile) + menu compte verre dépoli (avatar/nom/courriel/KA-ID/chip rôle/lien groupe-ka.com/compte/déconnexion, JS vanilla via /api/auth/me). Env node .env: + KA_SSO_SECRET/KA_HUB_URL/ORAKA_BASE_URL/SESSION_SECRET. Testé bout-en-bout (ka_id partagé OK) + non-régression: landing 200, /api/search sémantique OK, 5 montages 200. Client enregistré côté hub (KA_SSO_SECRET_ORA_KA, redirects www.ora-ka.com/). | 2026-08-13 v2: « MON UNIVERS KA » sur la landing — oraka/hubfav.py (hub_list_all scope=all, cache 30s), GET /api/favorites (session, 401 sinon), section #kafavs sous la barre de recherche (visible si connecté ET ≥1 favori, masquée pendant une recherche, pastilles d'univers réutilisées .rcard/.rtag, lien gérer sur groupe-ka.com/compte). Testé bout-en-bout (♥ lou-ka visible sur ora-ka). | 2026-08-13 v3: README complet (badges shields, tableau métriques prod 400 498 inscriptions, 9 captures Playwright docs/screenshots/ régénérables via scripts/screenshots.mjs). Repo poussé sur https://git.spboucher.ai/ora-ka (spbgit, branche main). Rsync M4M64a (data/ et dist/ préservés) + pm2 restart ora-ka-web + healthz OK.", | |
| 706 | + "decommissioned": "2026-09-04", | |
| 707 | + "spbgit_repo": "ora-ka", | |
| 708 | + "backup": "M4M64a:~/cluster-cleanup-backup-20260904/ora-ka/ (.env, *.db, data/)", | |
| 709 | + "status": "retirée du cluster — processus arrêtés, répertoire supprimé, README « URL live hors fonction » ; source de vérité = repo spbgit" | |
| 710 | + }, | |
| 711 | + { | |
| 712 | + "app": "toit-ka", | |
| 713 | + "node": "M3U96a", | |
| 714 | + "port": 8097, | |
| 715 | + "domain": "www.toit-ka.com", | |
| 716 | + "process_manager": "pm2", | |
| 717 | + "label_or_name": "toit-ka-web + toit-ka-etl + toit-ka-ngrok", | |
| 718 | + "dir": "~/apps/toit-ka", | |
| 719 | + "start": "pm2 start .venv/bin/python --name toit-ka-web --interpreter none --cwd ~/apps/toit-ka -- run.py serve 8097 ; pm2 start .venv/bin/python --name toit-ka-etl --interpreter none --cwd ~/apps/toit-ka -- run.py watch 30 ; pm2 start /opt/homebrew/bin/ngrok --name toit-ka-ngrok --interpreter none -- http --url=www.toit-ka.com 8097", | |
| 720 | + "deployed": "2026-08-15", | |
| 721 | + "notes": "TOIT-KA — FUSION Lou-Ka × Immo-Ka : plateforme unifiée louer + acheter (85 503 annonces : 22 881 locations + 62 622 ventes). NE SCRAPE RIEN : toit-ka-etl tire toutes les 30 min des snapshots LECTURE SEULE des BD de prod via LAN (ssh 192.168.2.82 lou-ka louka.db + 192.168.2.83 immo-ka immoka.db, sqlite3 .backup puis rsync -> data/replicas/), puis ETL -> data/toitka.db (table listings unifiée, colonne transaction_type louer|acheter ; règles de visibilité héritées : dédup Centris dup_hidden=0 + prix non nul côté vente ; annonces disparues -> active=0 -> fiche 410). NORMALISATION VILLES : toitka/villes.py (règles déterministes + élection par slug, ~3 300 variantes brutes -> table city_map auditable via run.py villes — corrige « Brossard ) », « Gatineau (Gatineau) », arrondissements collés 2 ordres, St-/Saint-, Québec City, Montrã©al…). Types unifiés toitka/typologie.py (décodage '). Backend FastAPI (toitka/web.py) + SSR SEO (seo.py : /louer/{ville}[/{type}], /acheter/{ville}[/{type}], /{tx}/type/{type}, /annonce/{uid}/{slug} 301+410, sitemaps, robots). Frontend React18+Vite+TS BI-ACCENT (html[data-mode] : lime=louer, rouge=acheter, terracotta=marque ; toggle central sur l'accueil), carte Ka Maps 3D (markers rent lime / sale encre), SSO KA ID (client toit-ka enregistré au hub groupe-ka.com — KA_SSO_SECRET_TOIT_KA dans .env.local du hub sur M3U96b, commit 9888039) + favoris hub. .env node chmod 600 : TOITKA_BASE_URL, KA_SSO_SECRET, AUTH_SECRET, TOITKA_REPLICA_SOURCES. Logs : ~/.pm2/logs/toit-ka-*.log. Node imposé par l'utilisateur (règle M3U96a=staging assouplie). pm2 save fait. Source laptop : ~/Desktop/toit-ka. Repo : https://git.spboucher.ai/toit-ka. En-tête auteur Simon-Pierre Boucher — contact@spboucher.ai partout. lou-ka.com et immo-ka.com INTACTS (lecture seule).", | |
| 722 | + "decommissioned": "2026-09-04", | |
| 723 | + "spbgit_repo": "toit-ka", | |
| 724 | + "backup": "M3U96a:~/cluster-cleanup-backup-20260904/toit-ka/ (.env, *.db, data/)", | |
| 725 | + "status": "retirée du cluster — processus arrêtés, répertoire supprimé, README « URL live hors fonction » ; source de vérité = repo spbgit" | |
| 726 | + }, | |
| 727 | + { | |
| 728 | + "app": "qc-election", | |
| 729 | + "node": "M2U64", | |
| 730 | + "port": 8140, | |
| 731 | + "domain": "www.qc-election.com", | |
| 732 | + "process_manager": "pm2", | |
| 733 | + "label_or_name": "qc-election / qc-election-ngrok", | |
| 734 | + "dir": "~/apps/qc-election", | |
| 735 | + "start": "pm2 start ~/apps/qc-election/ecosystem.config.js", | |
| 736 | + "deployed": "2026-08-30", | |
| 737 | + "notes": "Forecast électoral QC 2026 v3.1.0 « 127 » « Au-delà des sondages » : Kalman/alr + prior de fondamentaux (satisfaction Léger via Firecrawl, w(t) décroissant) + votes réels des 4 partielles dans le filtre + nudge médias ±0,35 pp + ensemble 88/12 Polymarket sur P(1er) + veille Firecrawl continue (radar sondages/satisfaction/presse, clé dans backend/.env) + page /signaux + décomposition /api/beyond. Monte Carlo 25k + 4×8k (décomposition), backtest 2022 comparatif. ADMIN_TOKEN + FIRECRAWL_API_KEY dans backend/.env (chargé par config.py). Pipeline auto 6 h. Repo gitsrv à jour (03c9ead); source laptop: ~/Desktop/qc-election. v2.1 : attention Wikipédia (turbulence → variance ×≤1,45), sondage synthétique LLM (96 strates, expérimental poids nul), page /aujourdhui (Le point du jour, /api/today). v3 : Social Intelligence (actor Apify qc-social-pulse, APIFY_TOKEN+SERPER_API_KEY dans backend/.env), validation/ablation, battlegrounds, /circonscriptions, API v3, design system QC Signal. v3.1 : replay LOEO 6 élections (/api/replay), σ_industrie appris 0,22, next-poll prediction, frontend QC Signal (hémicycle 127, strip).", | |
| 738 | + "decommissioned": "2026-09-04", | |
| 739 | + "spbgit_repo": "qc-election", | |
| 740 | + "backup": "M2U64:~/cluster-cleanup-backup-20260904/qc-election/ (.env, *.db, data/)", | |
| 741 | + "status": "retirée du cluster — processus arrêtés, répertoire supprimé, README « URL live hors fonction » ; source de vérité = repo spbgit" | |
| 742 | + }, | |
| 743 | + { | |
| 744 | + "app": "home-ka", | |
| 745 | + "node": "M4M36", | |
| 746 | + "port": 8099, | |
| 747 | + "domain": "www.home-ka.com", | |
| 748 | + "process_manager": "pm2", | |
| 749 | + "label_or_name": "home-ka-web + home-ka-sync + home-ka-ngrok", | |
| 750 | + "dir": "~/apps/home-ka", | |
| 751 | + "start": "cd ~/apps/home-ka && pm2 start .venv/bin/python --name home-ka-web --interpreter none -- run.py serve 8099 ; pm2 start .venv/bin/python --name home-ka-sync --interpreter none -- run.py watch 60 ; pm2 start /opt/homebrew/bin/ngrok --name home-ka-ngrok --interpreter none -- http --url=www.home-ka.com 8099", | |
| 752 | + "deployed": "2026-08-28", | |
| 753 | + "notes": "⏸ EN PAUSE depuis le 2026-08-28 (demande utilisateur) : pm2 stop home-ka-web/sync/ngrok (pm2 save fait — ne redémarreront pas au reboot), jobs nohup tués, dossier ~/apps/home-ka INTACT sur M4M36 (1,3 Go, data/homeka.db incluse). État gelé : 182 327 annonces actives (125 420 publiées), 352 848 propriétés, 85 sources (backend Firecrawl, clé dans .env), 630 brokerages. REPRISE : ssh M4M36 puis `pm2 start home-ka-web home-ka-sync home-ka-ngrok && pm2 save` — le domaine ngrok www.home-ka.com se rattache tout seul. | Home·Ka — agrégateur immobilier US (version américaine d'Immo-Ka), EN LIGNE sur https://www.home-ka.com (healthcheck 200 le 2026-08-28). Même architecture qu'Immo-Ka : FastAPI + SQLite WAL (data/homeka.db), pipeline SOURCE→connector→normalisation→property matching→dédup→canonique. Schéma aligné RESO Data Dictionary, séparation PROPERTY (table properties, clé APN/adresse normalisée) vs LISTING. Familles de connecteurs config-driven via table sources : reso (Web API OData — MLS Grid/Trestle/Bridge), rets, xml, json, jsonld (sites REW server-rendered), csv, sftp, arcgis (parcelles county→properties) + customs auto-découverts. Sources live v1 : michael_saunders + john_r_wood (jsonld, FL). Géocodage US Census batch gratuit + Nominatim budgété. Pipeline brokerages : seed 630 brokerages US réels, discovery.py inspecte (fingerprints IDX kvCORE/iHomefinder/REW/Delta…, RESO, contacts) et score (feed_probability, priority) ; admin /admin/sources /admin/connectors /admin/brokerages (token HOMEKA_ADMIN_TOKEN dans .env du nœud, header X-Admin-Token). Anti-bot : direct d'abord, escalade _resilient Oxylabs→Scrapfly→BrightData si bloqué (clés Scrapfly/Firecrawl copiées d'immo-ka). Frontend React/Vite forké d'immo-ka : anglais/USD, palette rouge/blanc/bleu US (accent bleu #1e4fd8, rouge Old Glory #b31942, fond blanc — changé du vert le 2026-08-28, commit 1e56b86), ka-maps vendorisé (vendor/ka-maps, preserveSymlinks). watch 60 min = sync + geocode + imgaudit + discovery (15/cycle). Git origin gitsrv:srv/git/home-ka.git (commit initial bb09ddb). NOTE: og.png/favicon encore l'artwork Immo-Ka; jeton Mapbox pk partagé — ajouter www.home-ka.com aux restrictions d'URL Mapbox si la carte ne charge pas.", | |
| 754 | + "decommissioned": "2026-09-04", | |
| 755 | + "spbgit_repo": "home-ka", | |
| 756 | + "backup": "M4M36:~/cluster-cleanup-backup-20260904/home-ka/ (.env, *.db, data/)", | |
| 757 | + "status": "retirée du cluster — processus arrêtés, répertoire supprimé, README « URL live hors fonction » ; source de vérité = repo spbgit" | |
| 758 | + } | |
| 759 | + ], | |
| 760 | + "superseded_by": "cluster-registry.json (copie de M1M32:~/dispatch/registry.json, orchestrateur maclustr-dispatch `mld`) — depuis le 2026-09-04 ce fichier n'est plus alimenté ; il garde l'historique et la section decommissioned." | |
| 761 | +} | |
| \ No newline at end of file | ||
added
cluster-gateway.sh
+147 −0
@@ -0,0 +1,147 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +# cluster-gateway.sh — M3U96a comme porte d'entrée (staging) du cluster MacLustr. | |
| 3 | +# | |
| 4 | +# Principe : on pousse un projet UNE fois laptop -> M3U96a (~/cluster-projects/<nom>/), | |
| 5 | +# puis M3U96a le diffuse aux autres noeuds en parallele via le reseau local (Tailscale/LAN). | |
| 6 | +# Le meme chemin ~/cluster-projects/<nom>/ existe ainsi sur tous les noeuds cibles. | |
| 7 | +# | |
| 8 | +# Usage : | |
| 9 | +# cluster-gateway.sh stage <dir-local> [nom] # laptop -> M3U96a | |
| 10 | +# cluster-gateway.sh fanout <nom> [noeud ...] # M3U96a -> noeuds (defaut: tous sauf M3U96a & M2U64) | |
| 11 | +# cluster-gateway.sh push <dir-local> [nom] # stage + fanout (defaut) | |
| 12 | +# cluster-gateway.sh list # projets stages sur M3U96a | |
| 13 | +# cluster-gateway.sh where <nom> # sur quels noeuds le projet est present | |
| 14 | +# cluster-gateway.sh clean <nom> [--everywhere] # efface de M3U96a (et de tous les noeuds) | |
| 15 | +# | |
| 16 | +# Options : --include-m2u64 (fanout/push : inclure le noeud prod M2U64) | |
| 17 | +# Doc complete : ~/Desktop/cluster-skill/GATEWAY.md | |
| 18 | +set -euo pipefail | |
| 19 | + | |
| 20 | +GATEWAY="M3U96a" | |
| 21 | +DOMAIN="maclustr.io" | |
| 22 | +STAGE_DIR="cluster-projects" | |
| 23 | +LAN_PREFIX_RE='192\.168\.2\.' # sous-reseau LAN du cluster | |
| 24 | +ALL_NODES="M3U96b M2U64 M4M64a M4M64b M4BP48 M4BP36 M4M36 M2M32 M2M32b M2M32c m4mc M1M32 m4ma m4mb m2m16 M3BA24 M3BA16 m2m8a m2m8b" | |
| 25 | + | |
| 26 | +usage() { sed -n '2,18p' "$0" | sed 's/^# \{0,1\}//'; exit 1; } | |
| 27 | + | |
| 28 | +CMD="${1:-}"; shift || true | |
| 29 | + | |
| 30 | +INCLUDE_M2U64=0 | |
| 31 | +ARGS=() | |
| 32 | +for a in "$@"; do | |
| 33 | + case "$a" in | |
| 34 | + --include-m2u64) INCLUDE_M2U64=1 ;; | |
| 35 | + *) ARGS+=("$a") ;; | |
| 36 | + esac | |
| 37 | +done | |
| 38 | +set -- "${ARGS[@]:-}" | |
| 39 | + | |
| 40 | +default_targets() { | |
| 41 | + local out="" | |
| 42 | + for n in $ALL_NODES; do | |
| 43 | + [ "$n" = "M2U64" ] && [ "$INCLUDE_M2U64" -eq 0 ] && continue | |
| 44 | + out="$out $n" | |
| 45 | + done | |
| 46 | + echo "$out" | |
| 47 | +} | |
| 48 | + | |
| 49 | +do_stage() { | |
| 50 | + local src="${1:?usage: stage <dir-local> [nom]}" | |
| 51 | + src="${src%/}" | |
| 52 | + [ -d "$src" ] || { echo "ERREUR: $src n'est pas un dossier" >&2; exit 2; } | |
| 53 | + local name="${2:-$(basename "$src")}" | |
| 54 | + echo ">> stage: $src -> $GATEWAY:~/$STAGE_DIR/$name/" | |
| 55 | + rsync -az --delete --rsync-path="mkdir -p $STAGE_DIR && rsync" \ | |
| 56 | + "$src/" "$GATEWAY:$STAGE_DIR/$name/" | |
| 57 | + echo "OK stage $name ($(du -sh "$src" | cut -f1))" | |
| 58 | +} | |
| 59 | + | |
| 60 | +do_fanout() { | |
| 61 | + local name="${1:?usage: fanout <nom> [noeud ...]}"; shift || true | |
| 62 | + local nodes="${*:-$(default_targets)}" | |
| 63 | + # Le SSH inter-noeuds via Tailscale est filtre (ACL) : le fanout passe par le LAN | |
| 64 | + # 192.168.2.x. Les IP LAN sont en DHCP -> on les resout en live depuis le laptop. | |
| 65 | + echo ">> resolution des IP LAN des cibles ($(echo $nodes | wc -w | tr -d ' ') noeuds)..." | |
| 66 | + local tmp; tmp=$(mktemp -d) | |
| 67 | + local n | |
| 68 | + for n in $nodes; do | |
| 69 | + ( ip=$(ssh -n -o ConnectTimeout=8 "$n" \ | |
| 70 | + "ifconfig | awk '/inet $LAN_PREFIX_RE/{print \$2; exit}'" 2>/dev/null || true) | |
| 71 | + echo "${ip:-NONE}" >"$tmp/$n" ) & | |
| 72 | + done | |
| 73 | + wait | |
| 74 | + local pairs="" | |
| 75 | + for n in $nodes; do pairs="$pairs $n=$(cat "$tmp/$n" 2>/dev/null || echo NONE)"; done | |
| 76 | + rm -rf "$tmp" | |
| 77 | + echo ">> fanout depuis $GATEWAY via LAN: $name ->$(echo "$pairs" | tr ' ' '\n' | grep -c '=' | xargs echo) noeuds" | |
| 78 | + # shellcheck disable=SC2087 | |
| 79 | + ssh -A "$GATEWAY" "NAME='$name' PAIRS='$pairs' bash -s" <<'REMOTE' | |
| 80 | +set -u | |
| 81 | +SRC="$HOME/cluster-projects/$NAME/" | |
| 82 | +[ -d "$SRC" ] || { echo "ERREUR: $SRC absent sur la passerelle — faire stage d abord" >&2; exit 2; } | |
| 83 | +TMP=$(mktemp -d) | |
| 84 | +for pair in $PAIRS; do | |
| 85 | + n="${pair%%=*}"; ip="${pair##*=}" | |
| 86 | + if [ "$ip" = "NONE" ]; then echo "FAIL (hors LAN cluster ou injoignable)" >"$TMP/$n"; continue; fi | |
| 87 | + ( rsync -az --delete \ | |
| 88 | + -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=8" \ | |
| 89 | + --rsync-path="mkdir -p cluster-projects && rsync" \ | |
| 90 | + "$SRC" "$ip:cluster-projects/$NAME/" \ | |
| 91 | + >/dev/null 2>"$TMP/$n.err" && echo "OK ($ip)" >"$TMP/$n" || echo "FAIL ($ip)" >"$TMP/$n" ) & | |
| 92 | +done | |
| 93 | +wait | |
| 94 | +FAILED=0 | |
| 95 | +for pair in $PAIRS; do | |
| 96 | + n="${pair%%=*}" | |
| 97 | + st=$(cat "$TMP/$n" 2>/dev/null || echo FAIL) | |
| 98 | + printf " %-8s %s\n" "$n" "$st" | |
| 99 | + case "$st" in FAIL*) FAILED=1; [ -f "$TMP/$n.err" ] && sed 's/^/ /' "$TMP/$n.err" | grep -v "known hosts" | head -2 ;; esac | |
| 100 | +done | |
| 101 | +rm -rf "$TMP" | |
| 102 | +exit $FAILED | |
| 103 | +REMOTE | |
| 104 | + echo "OK fanout $name termine" | |
| 105 | +} | |
| 106 | + | |
| 107 | +do_list() { | |
| 108 | + echo ">> projets stages sur $GATEWAY:~/$STAGE_DIR/" | |
| 109 | + ssh "$GATEWAY" "du -sh $STAGE_DIR/*/ 2>/dev/null || echo ' (aucun projet stage)'" | |
| 110 | +} | |
| 111 | + | |
| 112 | +do_where() { | |
| 113 | + local name="${1:?usage: where <nom>}" | |
| 114 | + echo ">> presence de '$name' sur le cluster :" | |
| 115 | + for n in $GATEWAY $ALL_NODES; do | |
| 116 | + ( sz=$(ssh -o ConnectTimeout=6 "$n.$DOMAIN" "du -sh $STAGE_DIR/$name 2>/dev/null | cut -f1" 2>/dev/null) | |
| 117 | + printf " %-8s %s\n" "$n" "${sz:-—}" ) & | |
| 118 | + done | |
| 119 | + wait | |
| 120 | +} | |
| 121 | + | |
| 122 | +do_clean() { | |
| 123 | + local name="${1:?usage: clean <nom> [--everywhere]}" | |
| 124 | + local everywhere=0 | |
| 125 | + [ "${2:-}" = "--everywhere" ] && everywhere=1 | |
| 126 | + if [ "$everywhere" -eq 1 ]; then | |
| 127 | + echo ">> clean '$name' sur TOUS les noeuds" | |
| 128 | + for n in $GATEWAY $ALL_NODES; do | |
| 129 | + ( ssh -o ConnectTimeout=6 "$n.$DOMAIN" "rm -rf $STAGE_DIR/$name" 2>/dev/null \ | |
| 130 | + && printf " %-8s OK\n" "$n" || printf " %-8s (injoignable)\n" "$n" ) & | |
| 131 | + done | |
| 132 | + wait | |
| 133 | + else | |
| 134 | + echo ">> clean '$name' sur $GATEWAY uniquement" | |
| 135 | + ssh "$GATEWAY" "rm -rf $STAGE_DIR/$name" && echo " $GATEWAY OK" | |
| 136 | + fi | |
| 137 | +} | |
| 138 | + | |
| 139 | +case "$CMD" in | |
| 140 | + stage) do_stage "$@" ;; | |
| 141 | + fanout) do_fanout "$@" ;; | |
| 142 | + push) do_stage "$1" "${2:-}"; do_fanout "${2:-$(basename "${1%/}")}" ;; | |
| 143 | + list) do_list ;; | |
| 144 | + where) do_where "$@" ;; | |
| 145 | + clean) do_clean "$@" ;; | |
| 146 | + *) usage ;; | |
| 147 | +esac | |
added
dossier/README.md
+16 −0
@@ -0,0 +1,16 @@ | ||
| 1 | +# Dossier technique MacLustr (PDF) | |
| 2 | + | |
| 3 | +Sortie : `cluster-skill/MACLUSTR-DOSSIER.pdf` (copie datée sur le Bureau). A4 paysage, couverture sombre + corps clair, ~14 pages. | |
| 4 | + | |
| 5 | +Sources : `../INVENTAIRE-MACLUSTR.md` (tables mesurées), `data.json` (export de `maclustr-www/public/data.js`), `live.json` (`https://www.maclustr.io/api/live`), `map.png` (capture de www.maclustr.io/map). | |
| 6 | + | |
| 7 | +```bash | |
| 8 | +cd ~/Desktop/cluster-skill/dossier | |
| 9 | +curl -s https://www.maclustr.io/api/live > live.json | |
| 10 | +node -e "const w={};require('vm').runInNewContext(require('fs').readFileSync(process.env.HOME+'/Desktop/Cluster/maclustr-www/public/data.js','utf8'),{window:w});require('fs').writeFileSync('data.json',JSON.stringify(w.MACLUSTR,null,1))" | |
| 11 | +python3 build.py # → cover.html + body.html | |
| 12 | +cd ~/Desktop/uqo-eval && cp ~/Desktop/cluster-skill/dossier/print.mjs _print.mjs && sed -i '' "s#const here = .*#const here = process.env.HOME + '/Desktop/cluster-skill/dossier';#" _print.mjs && node _print.mjs && rm _print.mjs # Playwright → cover.pdf + body.pdf | |
| 13 | +cd ~/Desktop/cluster-skill/dossier && python3 -c "import fitz;o=fitz.open();[o.insert_pdf(fitz.open(f)) for f in ('cover.pdf','body.pdf')];o.save('../MACLUSTR-DOSSIER.pdf',garbage=3,deflate=True)" | |
| 14 | +``` | |
| 15 | + | |
| 16 | +Mettre à jour `INVENTAIRE-MACLUSTR.md` et `data.js` avant de régénérer après tout ajout/retrait de machine. | |
added
dossier/body.html
+229 −0
@@ -0,0 +1,229 @@ | ||
| 1 | +<!doctype html><html lang='fr'><head><meta charset='utf-8'><title>MacLustr — Dossier technique</title><style>/* Dossier technique MacLustr — feuille d'impression (A4 paysage) */ | |
| 2 | +@page { size: A4 landscape; margin: 13mm 14mm 15mm 14mm; } | |
| 3 | +:root { | |
| 4 | + --ink: #0f1115; --ink-2: #4b5060; --ink-3: #868b99; --line: #dfe2ea; --line-2: #c9cdd8; | |
| 5 | + --paper: #ffffff; --paper-2: #f5f6f9; --paper-3: #eceef4; | |
| 6 | + --accent: #2563eb; --accent-2: #1d4ed8; --accent-dim: #e8effd; --alt: #7c3aed; | |
| 7 | + --good: #16a34a; --bad: #dc2626; --off: #9aa0ae; | |
| 8 | + --sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", Inter, Arial, sans-serif; | |
| 9 | + --mono: "SF Mono", ui-monospace, Menlo, Consolas, monospace; | |
| 10 | +} | |
| 11 | +* { box-sizing: border-box; } | |
| 12 | +html, body { margin: 0; padding: 0; } | |
| 13 | +body { font: 9pt/1.42 var(--sans); color: var(--ink); background: var(--paper); -webkit-print-color-adjust: exact; print-color-adjust: exact; } | |
| 14 | +h1, h2, h3, h4 { margin: 0; letter-spacing: -.02em; line-height: 1.12; } | |
| 15 | +p { margin: 0 0 6px; } | |
| 16 | +code { font: 8.4pt var(--mono); background: var(--paper-2); border: 1px solid var(--line); border-radius: 3px; padding: 0 3px; color: var(--accent-2); } | |
| 17 | +strong { font-weight: 650; } | |
| 18 | +.mono { font-family: var(--mono); } | |
| 19 | +.small { font-size: 8pt; } | |
| 20 | +.faint { color: var(--ink-3); } | |
| 21 | +.r { text-align: right; } | |
| 22 | +.nw { white-space: nowrap; } | |
| 23 | +.logo { width: 22px; height: 22px; color: var(--accent); vertical-align: middle; } | |
| 24 | +.brand { display: inline-flex; align-items: center; gap: 8px; font-weight: 700; font-size: 13pt; letter-spacing: -.02em; } | |
| 25 | +.eyebrow { font-size: 7.6pt; text-transform: uppercase; letter-spacing: .14em; color: var(--ink-3); font-weight: 600; margin: 0 0 8px; } | |
| 26 | + | |
| 27 | +/* ---------- couverture ---------- */ | |
| 28 | +body.cover { background: #09090b; color: #f4f4f5; width: 297mm; height: 210mm; overflow: hidden; } | |
| 29 | +.cover .kpi { background: #111114; border: 0; border-radius: 0; } | |
| 30 | +.cover .kpi b { color: #fff; } .cover .kpi span { color: #a1a1aa; } .cover .kpi small { color: #6b6b76; } | |
| 31 | +.cv { position: relative; width: 297mm; height: 210mm; overflow: hidden; padding: 14mm 18mm; display: flex; flex-direction: column; justify-content: space-between; background: radial-gradient(70% 55% at 80% 0%, rgba(96,165,250,.16), transparent 70%), #09090b; } | |
| 32 | +.cv::after { content: ""; position: absolute; inset: 0; background-image: linear-gradient(rgba(255,255,255,.035) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,.035) 1px, transparent 1px); background-size: 14mm 14mm; -webkit-mask-image: radial-gradient(ellipse at 50% 0%, #000 0%, transparent 75%); mask-image: radial-gradient(ellipse at 50% 0%, #000 0%, transparent 75%); pointer-events: none; } | |
| 33 | +.cv > * { position: relative; z-index: 1; } | |
| 34 | +.cv .brand { color: #f4f4f5; } | |
| 35 | +.cv .logo { color: #60a5fa; } | |
| 36 | +.cv-top { display: flex; justify-content: space-between; align-items: center; color: #a1a1aa; font-size: 9pt; } | |
| 37 | +.cv-mid { max-width: 190mm; } | |
| 38 | +.cv .eyebrow { color: #a1a1aa; } | |
| 39 | +.cv h1 { font-size: 52pt; font-weight: 750; letter-spacing: -.04em; line-height: .96; color: #fff; } | |
| 40 | +.cv h1 span { color: #6b6b76; } | |
| 41 | +.cv .sub { margin-top: 7mm; font-size: 11.5pt; line-height: 1.5; color: #c9cbd3; max-width: 160mm; } | |
| 42 | +.cv-facts { display: grid; grid-template-columns: repeat(6, 1fr); border: 1px solid #26262d; border-radius: 10px; overflow: hidden; background: #111114; } | |
| 43 | +.cv-facts .kpi { padding: 5mm 5mm; border-right: 1px solid #26262d; } | |
| 44 | +.cv-facts .kpi:last-child { border-right: 0; } | |
| 45 | +.cv-facts .kpi b { display: block; font: 600 22pt/1.05 var(--mono); letter-spacing: -.03em; color: #fff; } | |
| 46 | +.cv-facts .kpi span { display: block; color: #a1a1aa; font-size: 8.6pt; margin-top: 2px; } | |
| 47 | +.cv-facts .kpi small { display: block; color: #6b6b76; font: 7.6pt var(--mono); margin-top: 4px; } | |
| 48 | +.cv-bot { display: flex; justify-content: space-between; color: #6b6b76; font-size: 8.4pt; } | |
| 49 | + | |
| 50 | +/* ---------- corps ---------- */ | |
| 51 | +.sec { break-before: page; } | |
| 52 | +.sec:first-child { break-before: auto; } | |
| 53 | +.sec-h { display: flex; align-items: baseline; gap: 12px; border-bottom: 2px solid var(--ink); padding-bottom: 6px; margin-bottom: 10px; } | |
| 54 | +.sec-h .num { font: 600 11pt var(--mono); color: var(--accent); } | |
| 55 | +.sec-h h2 { font-size: 20pt; font-weight: 700; } | |
| 56 | +h3 { font-size: 12pt; font-weight: 650; margin: 10px 0 5px; break-after: avoid; display: flex; align-items: baseline; gap: 8px; } | |
| 57 | +h3 .cnt { font: 500 8.5pt var(--mono); color: var(--ink-3); } | |
| 58 | +h4 { font-size: 9.5pt; font-weight: 650; color: var(--ink-2); text-transform: uppercase; letter-spacing: .08em; margin-bottom: 8px; } | |
| 59 | +.lead { color: var(--ink-2); font-size: 9.1pt; max-width: 245mm; margin-bottom: 4px; } | |
| 60 | +.lead.big { font-size: 12pt; line-height: 1.5; color: var(--ink); margin-bottom: 10px; } | |
| 61 | +.note { color: var(--ink-3); font-size: 8.4pt; margin-top: 4px; } | |
| 62 | +.two { display: grid; grid-template-columns: 1fr 1fr; gap: 10mm; align-items: start; } | |
| 63 | +.three-col { display: grid; grid-template-columns: 1.1fr 1fr 1.2fr; gap: 5mm; align-items: start; } | |
| 64 | +.kpis { display: grid; grid-template-columns: repeat(6, 1fr); gap: 4mm; margin: 6px 0 4px; } | |
| 65 | +.kpis.three { grid-template-columns: repeat(3, 1fr); } | |
| 66 | +.kpi { border: 1px solid var(--line); border-radius: 8px; padding: 7px 10px; background: var(--paper); break-inside: avoid; } | |
| 67 | +.kpi b { display: block; font: 600 17pt/1.05 var(--mono); letter-spacing: -.03em; } | |
| 68 | +.kpi span { display: block; color: var(--ink-2); font-size: 8.4pt; margin-top: 2px; } | |
| 69 | +.kpi small { display: block; color: var(--ink-3); font: 7.4pt var(--mono); margin-top: 4px; } | |
| 70 | +.toc { border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; background: var(--paper-2); } | |
| 71 | +.toc div { display: flex; gap: 10px; padding: 3px 0; border-bottom: 1px dashed var(--line); font-size: 9pt; } | |
| 72 | +.toc div:last-child { border-bottom: 0; } | |
| 73 | +.toc .num { font: 600 8.4pt var(--mono); color: var(--accent); width: 18px; } | |
| 74 | + | |
| 75 | +/* tables */ | |
| 76 | +table.t { width: 100%; border-collapse: collapse; margin: 4px 0 10px; font-size: 8.2pt; break-inside: auto; } | |
| 77 | +table.t th { text-align: left; font-size: 7.4pt; text-transform: uppercase; letter-spacing: .06em; color: var(--ink-3); font-weight: 600; padding: 5px 6px; border-bottom: 1.5px solid var(--ink); white-space: nowrap; } | |
| 78 | +table.t th.r { text-align: right; } | |
| 79 | +table.t td { padding: 4px 6px; border-bottom: 1px solid var(--line); vertical-align: top; overflow-wrap: anywhere; } | |
| 80 | +table.t tr { break-inside: avoid; } | |
| 81 | +table.t thead { display: table-header-group; } | |
| 82 | +table.t td.id { font: 600 8.4pt var(--mono); white-space: nowrap; color: var(--ink); } | |
| 83 | +table.t td.mono, .mono { font-family: var(--mono); } | |
| 84 | +table.t td code { font-size: 7.6pt; } | |
| 85 | +table.t.hw { font-size: 7.5pt; } | |
| 86 | +table.t.hw td { padding: 3px 4px; line-height: 1.3; } | |
| 87 | +table.t.over td:first-child, table.t.models td:first-child { font-weight: 500; } | |
| 88 | +table.t.roles td { font-size: 7.4pt; padding: 3px 5px; line-height: 1.3; } | |
| 89 | +.roles-page { break-before: page; } | |
| 90 | +.keep { break-inside: avoid; } | |
| 91 | +table.t.ovh td { font-size: 7.4pt; line-height: 1.3; } | |
| 92 | +table.t.ovh td:nth-child(6) { min-width: 34mm; } table.t.ovh td code { white-space: normal; word-break: break-all; } | |
| 93 | +.kpis.stack { grid-template-columns: 1fr; gap: 3mm; } | |
| 94 | +table.t.tun td:first-child { font: 600 8pt var(--mono); } | |
| 95 | +table.t.apps td { font-size: 7.8pt; padding: 3px 6px; } | |
| 96 | +.app-group { margin-bottom: 4px; } | |
| 97 | +table.t.heat td, table.t.bars td { vertical-align: middle; } | |
| 98 | +table.t.bars td { padding: 3px 6px; } | |
| 99 | +table.t.bars td small { line-height: 1.1; } | |
| 100 | +td small { color: var(--ink-3); font-size: 7.4pt; } | |
| 101 | + | |
| 102 | +/* barres */ | |
| 103 | +.meter { white-space: nowrap; } | |
| 104 | +.bar { display: inline-block; width: 52%; height: 6px; border-radius: 3px; background: var(--paper-3); vertical-align: middle; overflow: hidden; margin-right: 6px; } | |
| 105 | +.bar i { display: block; height: 100%; background: var(--accent); border-radius: 3px; } | |
| 106 | +.bar.alt i { background: var(--alt); } | |
| 107 | +.meter.wide .bar { width: 58%; } | |
| 108 | +.meter .v { font: 7.8pt var(--mono); color: var(--ink-2); vertical-align: middle; } | |
| 109 | + | |
| 110 | +/* statuts */ | |
| 111 | +.st { display: inline-block; padding: 1px 7px; border-radius: 999px; font-size: 7.2pt; font-weight: 600; border: 1px solid var(--line-2); color: var(--ink-2); white-space: nowrap; } | |
| 112 | +.st::before { content: ""; display: inline-block; width: 5px; height: 5px; border-radius: 50%; background: var(--off); margin-right: 5px; vertical-align: middle; } | |
| 113 | +.st.on { color: #166534; border-color: #bbf7d0; background: #f0fdf4; } .st.on::before { background: var(--good); } | |
| 114 | +.st.bad { color: #991b1b; border-color: #fecaca; background: #fef2f2; } .st.bad::before { background: var(--bad); } | |
| 115 | +.st.off { color: var(--ink-3); background: var(--paper-2); } | |
| 116 | + | |
| 117 | +/* fiches, panneaux */ | |
| 118 | +.fiches p { padding: 6px 9px; border-left: 3px solid var(--accent); background: var(--paper-2); border-radius: 0 6px 6px 0; margin-bottom: 5px; font-size: 8pt; break-inside: avoid; } | |
| 119 | +.panel { border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; margin-bottom: 8px; break-inside: avoid; } | |
| 120 | +.flow { margin: 0; padding: 0; list-style: none; counter-reset: f; } | |
| 121 | +.flow li { display: grid; grid-template-columns: 20px 1fr; gap: 8px; align-items: start; padding: 5px 0; border-bottom: 1px dashed var(--line); font-size: 8.6pt; } | |
| 122 | +.flow li:last-child { border-bottom: 0; } | |
| 123 | +.flow li::before { counter-increment: f; content: counter(f, decimal-leading-zero); font: 600 8pt var(--mono); color: var(--accent); } | |
| 124 | +.flow li b { display: block; font-weight: 650; } | |
| 125 | +.flow li div span { color: var(--ink-2); display: block; } | |
| 126 | +.gw { display: grid; grid-template-columns: 70px 1fr; gap: 2px 10px; padding: 6px 0; border-bottom: 1px dashed var(--line); font-size: 8.6pt; } | |
| 127 | +.gw:last-child { border-bottom: 0; } | |
| 128 | +.gw b { font: 600 9pt var(--mono); grid-row: span 2; align-self: center; } | |
| 129 | +.gw span { color: var(--ink-2); } | |
| 130 | +.gw .mono { font-size: 7.8pt; color: var(--ink-3); } | |
| 131 | + | |
| 132 | +/* carte et sites */ | |
| 133 | +figure.map { margin: 0 0 8px; break-inside: avoid; } | |
| 134 | +figure.map img { width: 100%; height: 78mm; object-fit: cover; object-position: center 40%; border-radius: 8px; border: 1px solid var(--line); display: block; } | |
| 135 | +figure.map figcaption { font-size: 7.8pt; color: var(--ink-3); margin-top: 4px; } | |
| 136 | +.sites { display: grid; grid-template-columns: repeat(4, 1fr); gap: 3mm; margin-top: 6px; } | |
| 137 | +.site { border: 1px solid var(--line); border-radius: 8px; padding: 7px 9px; break-inside: avoid; } | |
| 138 | +.site .s-h { display: flex; justify-content: space-between; align-items: baseline; gap: 6px; font-size: 9.8pt; } | |
| 139 | +.site .s-h .mono { font-size: 8pt; color: var(--ink-2); } | |
| 140 | +.site .s-r { color: var(--ink-3); font-size: 7.8pt; margin: 2px 0 6px; } | |
| 141 | +.site p { font-size: 7.5pt; color: var(--ink-2); min-height: 20px; margin-bottom: 3px; } | |
| 142 | +.site .s-n { display: flex; flex-wrap: wrap; gap: 3px; } | |
| 143 | +.site .s-n span { font: 7.4pt var(--mono); padding: 1px 5px; border-radius: 4px; background: var(--paper-2); border: 1px solid var(--line); } | |
| 144 | + | |
| 145 | +.end { display: flex; align-items: center; gap: 8px; margin-top: 18px; color: var(--ink-3); font: 8.4pt var(--mono); } | |
| 146 | +</style></head> | |
| 147 | +<body> | |
| 148 | +<section class='sec'><div class='sec-h'><span class='num'>1</span><h2>Vue d'ensemble</h2></div> | |
| 149 | +<div class='two'> | |
| 150 | + <div> | |
| 151 | + <p class='lead big'>MacLustr est un cluster privé de 33 Macs Apple Silicon et 4 serveurs Linux OVHcloud. Il totalise <strong>484 cœurs CPU</strong>, <strong>812 cœurs GPU</strong> et <strong>1,38 To de mémoire</strong>, et héberge <strong>36 applications</strong> en production, dont 35 sites publics servis en HTTPS par le MacLustr Tunnel.</p> | |
| 152 | + <p>Les compteurs « nominaux » du cluster (<code>NODES.md</code>, <code>CLAUDE.md</code>, site www.maclustr.io) comptent 32 cœurs pour chaque M3 Ultra ; <code>system_profiler</code> et <code>mld scan</code> en voient 28 (20 performance + 8 efficience). Ce document donne les valeurs mesurées.</p><p><strong>Architecture en trois plans.</strong> (1) La <strong>passerelle interne M1M32</strong> héberge le git personnel spbgit et l'orchestrateur <code>mld</code>, qui place chaque application sur le nœud le plus efficient, la supervise (<code>mld heal</code> toutes les 5 min) et pousse le registre aux consommateurs. (2) L'agent <strong>maclustr-agentd</strong> (M4M64a:9210) sonde tous les nœuds pour les apps macOS et iOS MacLustr et le site www.maclustr.io. (3) La <strong>passerelle publique BHS64</strong> (OVH Beauharnois) termine le TLS de 36 domaines avec Caddy et les relaie aux nœuds par le tunnel WireGuard « MacLustr Tunnel » (hub 10.67.0.1, 13 pairs) ; R9128 (Gravelines) est le hub de secours. Il n'y a plus de ngrok.</p><p><strong>Accès.</strong> Tout passe par <code>ssh <alias></code> depuis le laptop (<code>~/.ssh/config</code>) avec clés ed25519 et agent forwarding ; trois clés publiques sont autorisées partout : <code>maclustr-cluster</code> (laptop), <code>maclustr-m1m32</code> (passerelle mld) et <code>maclustr-agentd</code> (agent). Les mots de passe sudo et identifiants DeskIn/VNC ne figurent pas ici : voir <code>NODES.md</code>.</p> | |
| 153 | + </div> | |
| 154 | + <div> | |
| 155 | + <div class='toc'><h4>Sommaire</h4><div><span class='num'>1</span>Vue d'ensemble</div><div><span class='num'>2</span>Nœuds internes — LAN Saint-Augustin</div><div><span class='num'>3</span>Macs dédiés loués hors site</div><div><span class='num'>4</span>Serveurs dédiés OVHcloud</div><div><span class='num'>5</span>Topologie publique : tunnel et sites</div><div><span class='num'>6</span>Emplacements et carte</div><div><span class='num'>7</span>Répartitions et capacité</div><div><span class='num'>8</span>Applications hébergées</div><div><span class='num'>9</span>Instantané live</div><div><span class='num'>10</span>Sources et méthode</div></div> | |
| 156 | + </div> | |
| 157 | +</div> | |
| 158 | +<h3>Par catégorie</h3> | |
| 159 | +<table class='t over'><thead><tr><th>Catégorie</th><th class=r>Machines</th><th class=r>Cœurs CPU</th><th class=r>RAM</th><th>Stockage (brut)</th><th>Où</th></tr></thead><tbody><tr><td class="id"><strong>1. Nœuds internes — LAN MacLustr</strong></td><td class="r nw">21 Macs</td><td class="r nw">286 (nominal 294)</td><td class="r nw">784 Go</td><td>≈ 13,1 To</td><td>Saint-Augustin-de-Desmaures, Québec</td></tr><tr><td class="id"><strong>2. Macs loués hors site</strong></td><td class="r nw">12 Macs</td><td class="r nw">114</td><td class="r nw">240 Go</td><td>≈ 6,9 To</td><td>Turquie, États-Unis, Irlande, Chypre</td></tr><tr><td class="id"><strong>3. Serveurs dédiés OVHcloud (Linux)</strong></td><td class="r nw">4 serveurs</td><td class="r nw">38 c / 76 threads x86</td><td class="r nw">384 Go</td><td>4 × 2 × 960 Go NVMe (RAID 1)</td><td>Beauharnois (Québec) ×3, Gravelines (France) ×1</td></tr><tr><td class="id"><strong>Total</strong></td><td class="r nw"><strong>37 machines</strong></td><td class="r nw"><strong>400 cœurs Apple + 38 cœurs AMD</strong></td><td class="r nw"><strong>1 408 Go</strong></td><td>≈ 28 To</td><td>7 sites, 5 pays</td></tr></tbody></table> | |
| 160 | +<div class='kpis'><div class='kpi'><b>21</b><span>Macs sur le LAN</span><small>Saint-Augustin</small></div><div class='kpi'><b>12</b><span>Macs loués</span><small>Istanbul · Atlanta · Dublin · Limassol</small></div><div class='kpi'><b>4</b><span>serveurs OVHcloud</span><small>Beauharnois · Gravelines</small></div><div class='kpi'><b>3</b><span>passerelles</span><small>M1M32 · BHS64 · R9128</small></div><div class='kpi'><b>2</b><span>hubs WireGuard</span><small>17 pairs</small></div><div class='kpi'><b>38</b><span>routes HTTPS</span><small>Caddy · Let’s Encrypt</small></div></div> | |
| 161 | +</section> | |
| 162 | + | |
| 163 | +<section class='sec'><div class='sec-h'><span class='num'>2</span><h2>Nœuds internes — LAN Saint-Augustin-de-Desmaures</h2></div><p class=lead>Réseau <code>192.168.2.x</code>, domaine <code>maclustr.io</code>, Tailscale présent mais <strong>SSH inter-nœuds via Tailscale bloqué</strong> (ACL) : tout passe par les IP LAN. Utilisateur <code>simon-pierreboucher</code> partout, home <code>/Users/simon-pierreboucher</code>. Tous durcis le 10 septembre 2026 (<code>mld harden --all</code> : ouverture de session automatique, pas de veille, redémarrage après coupure, mises à jour automatiques désactivées, FileVault désactivé sur les deux M3 Ultra). PM2 pour les apps, launchd seulement pour l'agent, les gardiens et la console.</p> | |
| 164 | +<h3>2a. Matériel et système <span class='cnt'>21 Macs</span></h3> | |
| 165 | +<table class='t hw'><thead><tr><th>Alias</th><th>Modèle (identifiant)</th><th>Puce (P + E)</th><th class=r>Cœurs</th><th class=r>RAM</th><th>SSD total / libre</th><th>N° de série</th><th>IP LAN</th><th>Tunnel wg1</th><th>macOS</th><th>État 10 sept. 22 h 30</th></tr></thead><tbody><tr><td class="nw id">M3U96a</td><td>Mac Studio (Mac15,14)</td><td>M3 Ultra (20 + 8)</td><td class="r nw">28</td><td class="r nw">96 Go</td><td class="nw">995 / 487 Go</td><td>HM62TG64LP</td><td class="nw">192.168.2.87</td><td class="nw">10.67.0.10</td><td class="nw">26.6</td><td>en ligne, 17 processus PM2</td></tr><tr><td class="nw id">M3U96b</td><td>Mac Studio (Mac15,14)</td><td>M3 Ultra (20 + 8)</td><td class="r nw">28</td><td class="r nw">96 Go</td><td class="nw">995 / 414 Go</td><td>J3661QHQ7M</td><td class="nw">192.168.2.82</td><td class="nw">10.67.0.11</td><td class="nw">26.4</td><td>en ligne, 8 processus PM2</td></tr><tr><td class="nw id">M2U64</td><td>Mac Studio (Mac14,14)</td><td>M2 Ultra (16 + 8)</td><td class="r nw">24</td><td class="r nw">64 Go</td><td class="nw">995 / 401 Go</td><td>T99R24M36K</td><td class="nw">192.168.2.107</td><td class="nw">10.67.0.12</td><td class="nw">26.5</td><td>en ligne, 10 processus PM2</td></tr><tr><td class="nw id">M4M64a</td><td>Mac Studio (Mac16,9)</td><td>M4 Max (12 + 4)</td><td class="r nw">16</td><td class="r nw">64 Go</td><td class="nw">995 / 848 Go</td><td>YFQW7Y1734</td><td class="nw">192.168.2.83</td><td class="nw">10.67.0.13</td><td class="nw">26.5.1</td><td>en ligne, 9 processus PM2</td></tr><tr><td class="nw id">M4M64b</td><td>Mac Studio (Mac16,9)</td><td>M4 Max (12 + 4)</td><td class="r nw">16</td><td class="r nw">64 Go</td><td class="nw">995 / 799 Go</td><td>F163L9H422</td><td class="nw">192.168.2.78</td><td class="nw">10.67.0.14</td><td class="nw">26.5.1</td><td>en ligne, 19 processus PM2</td></tr><tr><td class="nw id">M4BP48</td><td>MacBook Pro (Mac16,5)</td><td>M4 Max (12 + 4)</td><td class="r nw">16</td><td class="r nw">48 Go</td><td class="nw">995 / 931 Go</td><td>DRXR751720</td><td class="nw">192.168.2.80</td><td class="nw">—</td><td class="nw">26.5.1</td><td>en ligne (98 j)</td></tr><tr><td class="nw id">M4BP36</td><td>MacBook Pro (Mac16,6)</td><td>M4 Max (10 + 4)</td><td class="r nw">14</td><td class="r nw">36 Go</td><td class="nw">995 / 945 Go</td><td>DJ616D4NPF</td><td class="nw">192.168.2.79</td><td class="nw">—</td><td class="nw">26.5.1</td><td>en ligne (rallumé, 6 j)</td></tr><tr><td class="nw id">M4M36</td><td>Mac Studio (Mac16,9)</td><td>M4 Max (10 + 4)</td><td class="r nw">14</td><td class="r nw">36 Go</td><td class="nw">494 / 445 Go</td><td>FXGPY9V7NT</td><td class="nw">192.168.2.69</td><td class="nw">10.67.0.17</td><td class="nw">26.5.2</td><td>en ligne, gardiens launchd</td></tr><tr><td class="nw id">M2M32</td><td>Mac Studio (Mac14,13)</td><td>M2 Max (8 + 4)</td><td class="r nw">12</td><td class="r nw">32 Go</td><td class="nw">494 / 365 Go</td><td>X5PF2TW170</td><td class="nw">192.168.2.90</td><td class="nw">10.67.0.18</td><td class="nw">26.5.1</td><td>en ligne</td></tr><tr><td class="nw id">M2M32b</td><td>Mac Studio (Mac14,13)</td><td>M2 Max (8 + 4)</td><td class="r nw">12</td><td class="r nw">32 Go</td><td class="nw">494 / 296 Go</td><td>VHKX7M65XC</td><td class="nw">192.168.2.77</td><td class="nw">—</td><td class="nw">26.5.1</td><td>en ligne</td></tr><tr><td class="nw id">M2M32c</td><td>Mac Studio (Mac14,13)</td><td>M2 Max (8 + 4)</td><td class="r nw">12</td><td class="r nw">32 Go</td><td class="nw">494 / 458 Go</td><td>W2QTCJ7TKG</td><td class="nw">192.168.2.89</td><td class="nw">—</td><td class="nw">26.5.1</td><td>en ligne</td></tr><tr><td class="nw id">m4mc</td><td>Mac mini (Mac16,11)</td><td>M4 Pro (8 + 4)</td><td class="r nw">12</td><td class="r nw">24 Go</td><td class="nw">494 / 456 Go</td><td>J742M2X1VC</td><td class="nw">192.168.2.75</td><td class="nw">—</td><td class="nw">26.5.1</td><td>en ligne</td></tr><tr><td class="nw id"><strong>M1M32</strong></td><td>Mac Studio (Mac13,1)</td><td>M1 Max (8 + 2)</td><td class="r nw">10</td><td class="r nw">32 Go</td><td class="nw">494 / 376 Go</td><td>LVC2Q4R716</td><td class="nw">192.168.2.76</td><td class="nw">10.67.0.2</td><td class="nw">26.5.1</td><td>en ligne, <strong>passerelle</strong></td></tr><tr><td class="nw id">m4ma</td><td>Mac mini (Mac16,10)</td><td>M4 (4 + 6)</td><td class="r nw">10</td><td class="r nw">24 Go</td><td class="nw">494 / 457 Go</td><td>N6NVTGFFVV</td><td class="nw">192.168.2.73</td><td class="nw">—</td><td class="nw">26.5.1</td><td>en ligne</td></tr><tr><td class="nw id">m4mb</td><td>Mac mini (Mac16,10)</td><td>M4 (4 + 6)</td><td class="r nw">10</td><td class="r nw">16 Go</td><td class="nw">245 / 209 Go</td><td>GYWPHYG6TX</td><td class="nw">192.168.2.74</td><td class="nw">—</td><td class="nw">26.5.1</td><td>en ligne</td></tr><tr><td class="nw id">m4md</td><td>Mac mini (Mac16,10)</td><td>M4 (4 + 6)</td><td class="r nw">10</td><td class="r nw">16 Go</td><td class="nw">245 Go</td><td>—</td><td class="nw">192.168.2.110 (dernière connue)</td><td class="nw">—</td><td class="nw">26.6.2</td><td><strong>hors ligne</strong> (éteint depuis la coupure du 10 sept.)</td></tr><tr><td class="nw id">m2m16</td><td>Mac mini (Mac14,12)</td><td>M2 Pro (6 + 4)</td><td class="r nw">10</td><td class="r nw">16 Go</td><td class="nw">494 / 458 Go</td><td>LFWH0GVGC6</td><td class="nw">192.168.2.85</td><td class="nw">—</td><td class="nw">26.4.1</td><td>en ligne (rallumé)</td></tr><tr><td class="nw id">M3BA24</td><td>MacBook Air (Mac15,12)</td><td>M3 (4 + 4)</td><td class="r nw">8</td><td class="r nw">24 Go</td><td class="nw">494 / 453 Go</td><td>M4KXY1V9CL</td><td class="nw">192.168.2.81</td><td class="nw">—</td><td class="nw">26.5.1</td><td>en ligne (rallumé, 6 j)</td></tr><tr><td class="nw id">M3BA16</td><td>MacBook Air (Mac15,12)</td><td>M3 (4 + 4)</td><td class="r nw">8</td><td class="r nw">16 Go</td><td class="nw">494 / 440 Go</td><td>LTQX17FW9W</td><td class="nw">192.168.2.70</td><td class="nw">—</td><td class="nw">26.5.1</td><td>en ligne (99 j)</td></tr><tr><td class="nw id">m2m8a</td><td>Mac mini (Mac14,3)</td><td>M2 (4 + 4)</td><td class="r nw">8</td><td class="r nw">8 Go</td><td class="nw">494 / 456 Go</td><td>WQF2N4L6RT</td><td class="nw">192.168.2.84</td><td class="nw">—</td><td class="nw">26.5.1</td><td>en ligne (rallumé)</td></tr><tr><td class="nw id">m2m8b</td><td>Mac mini (Mac14,3)</td><td>M2 (4 + 4)</td><td class="r nw">8</td><td class="r nw">8 Go</td><td class="nw">245 / 211 Go</td><td>Q339C7TMT7</td><td class="nw">192.168.2.72</td><td class="nw">—</td><td class="nw">26.5.1</td><td>en ligne</td></tr></tbody></table> | |
| 166 | +<p class='note'>: 21 Macs, 286 cœurs mesurés (294 nominaux), 784 Go, 20 en ligne.** 11 Mac Studio, 6 Mac mini, 2 MacBook Pro, 2 MacBook Air. 16 nœuds ont redémarré ensemble le 10 septembre (coupure de courant) et sont revenus seuls : PM2, tunnel wg1 et routes Caddy intacts. Autres équipements sur place : deux NAS UGREEN (DXP4800 Plus <code>192.168.2.173</code>, DH4300 Plus <code>192.168.2.175</code>).</p> | |
| 167 | +<div class='roles-page'><h3>2b. Rôle, runtimes et applications par nœud</h3> | |
| 168 | +<p class='lead'>Rôle, runtimes et applications par nœud</p> | |
| 169 | +<table class='t roles'><col style='width:7%'><col style='width:21%'><col style='width:18%'><col style='width:26%'><col style='width:28%'><thead><tr><th>Alias</th><th>Rôle</th><th>Runtimes détectés</th><th>Applications (port)</th><th>Processus PM2 / launchd</th></tr></thead><tbody><tr><td class="id">M3U96a</td><td>worker apps principal ; ex-passerelle LLM (llm.maclustr.io démantelée le 9 juin 2026)</td><td>brew, docker, git, node, pm2, pnpm, postgresql@17, py3.14, redis, tunnel, uv</td><td>admin-ka (3300, épinglé, launchd), fetcha (8220), job-ka (8096), polyllm (8240), qc26 (8200), rareindex (8210), sorti-ka (8120), uqo-imm1033 (8161), vrai-prix (8090)</td><td>fetcha-api, fetcha-web, job-ka-sync, job-ka-web, polyllm-web, qc26-engine, qc26-web, rareindex-api/-web/-worker, sorti-ka-sync/-web, uqo-imm1033, vrai-prix, vrai-prix-cost-sync, vrai-prix-immoka-sync, vrai-prix-ingest</td></tr><tr><td class="id">M3U96b</td><td><strong>réservé hfmarketdata</strong> (lac de données FirstRate <code>~/firstratedata</code>, 353 Go)</td><td>brew, docker, git, node, pm2, pnpm, py3.14, redis, tunnel</td><td>hfmarketdata (8090)</td><td>hfmarketdata-api, -contracts-backfill, -edgar-backfill, -edgar-incremental, -edgar-reconcile, -frd-refresh, -rowgroups, pm2-logrotate</td></tr><tr><td class="id">M2U64</td><td>worker apps, <strong>sans session graphique</strong> (services en LaunchDaemon)</td><td>brew, docker, git, node, pm2, pnpm, postgresql@17, py3.14, tunnel</td><td>food-ka (8097), maclustr-appmanager (8787), maclustr-console (3000), maclustr-nodepages (8003), spb-cloud (5001), spboucher-ai (3100), uqo-eval (8150), vquant (5000)</td><td>10 processus PM2 (food-ka, app-manager, console, nodepages, spb-cloud, spboucher-ai, uqo-eval + cron cost-sync, vquant)</td></tr><tr><td class="id">M4M64a</td><td>worker apps + <strong>maclustr-agentd :9210</strong> (sondes SSH de tout le cluster, API des apps macOS/iOS) + site vitrine www.maclustr.io</td><td>brew, docker, git, node, pm2, postgresql@17, py3.14, tunnel, uv</td><td>fabri-ka (8097), ka-stats (8140), lou-ka (8095), maclustr-agentd (9210, launchd), maclustr-www (8280, épinglé), pdb-api (8270), uqo-chat (8190)</td><td>fabri-ka-sync/-web, ka-stats, lou-ka-sync/-web, maclustr-www, pdb-api, uqo-chat-api, uqo-chat-sandbox</td></tr><tr><td class="id">M4M64b</td><td>worker apps le plus chargé (19 processus) ; Postgres 17 + Redis locaux pour api-ka</td><td>brew, docker, git, node, pm2, pnpm, postgresql@17, py3.13, py3.14, redis, tunnel</td><td>api-ka (8000, <code>/opt/api-ka</code>), auto-ka (8095), cancerindex (8250), groupe-ka (8110), immo-ka (8096), uqo-imm1003 (8160), websensor (8260)</td><td>apika-api, apika-scheduler, auto-ka-sync/-web, cancerindex-api/-backup/-web/-worker, groupe-ka (+ kaid-alerts, kaid-mf, rapport-quotidien, status-tick), immo-ka-sync/-web, uqo-imm1003, websensor-api/-engine/-web</td></tr><tr><td class="id">M4BP48</td><td>worker libre (portable)</td><td>brew, git, uv</td><td>—</td><td>—</td></tr><tr><td class="id">M4BP36</td><td>worker libre (portable)</td><td>brew, git</td><td>—</td><td>—</td></tr><tr><td class="id">M4M36</td><td><strong>gardiens Ka</strong> (agents de surveillance, launchd)</td><td>brew, git, node, pm2, py3.13, tunnel</td><td>ka2 (8799), ka4 (8899), ka6 (8999)</td><td>ka2, ka4, ka6 (LaunchAgents) + runners/courrier/pousseur</td></tr><tr><td class="id">M2M32</td><td>worker libre (ex-trouve-ka, volumes Docker conservés)</td><td>brew, docker, git, node, pm2, pnpm, py3.14, tunnel, uv</td><td>—</td><td>—</td></tr><tr><td class="id">M2M32b</td><td>worker libre (ex-flotte trouve-ka)</td><td>brew, docker, git, node, pm2, uv</td><td>—</td><td>—</td></tr><tr><td class="id">M2M32c</td><td>worker libre</td><td>git, uv</td><td>—</td><td>—</td></tr><tr><td class="id">m4mc</td><td>worker libre</td><td>git, uv</td><td>—</td><td>—</td></tr><tr><td class="id"><strong>M1M32</strong></td><td><strong>passerelle du cluster</strong> : spbgit (dépôts bare <code>~/srv/git/*.git</code>, git.spboucher.ai :7420), orchestrateur <code>mld</code> (<code>~/dispatch</code>), <code>mld heal</code> (LaunchDaemon io.maclustr.mld-heal, 5 min), alias <code>gitsrv</code> ; aucune autre app</td><td>brew, git, node, pm2, postgresql@17, tunnel, uv</td><td>spbgit (7420)</td><td>spbgit</td></tr><tr><td class="id">m4ma</td><td>worker libre</td><td>git, uv</td><td>—</td><td>—</td></tr><tr><td class="id">m4mb</td><td>worker libre</td><td>git</td><td>—</td><td>—</td></tr><tr><td class="id">m4md</td><td>worker libre (hors ligne)</td><td>—</td><td>—</td><td>—</td></tr><tr><td class="id">m2m16</td><td>worker libre</td><td>git</td><td>—</td><td>—</td></tr><tr><td class="id">M3BA24</td><td>worker libre (portable)</td><td>brew, git</td><td>—</td><td>—</td></tr><tr><td class="id">M3BA16</td><td>worker libre (portable)</td><td>brew, git</td><td>—</td><td>—</td></tr><tr><td class="id">m2m8a</td><td>worker libre (8 Go)</td><td>brew, git</td><td>—</td><td>—</td></tr><tr><td class="id">m2m8b</td><td>worker libre (8 Go)</td><td>git</td><td>—</td><td>—</td></tr></tbody></table></div> | |
| 170 | +</section> | |
| 171 | + | |
| 172 | +<section class='sec'><div class='sec-h'><span class='num'>3</span><h2>Macs dédiés loués hors site</h2></div><p class=lead>Douze Macs loués chez trois fournisseurs, ajoutés du 8 au 10 septembre 2026. Ils sont dans <code>mld</code> comme <code>REMOTE_NODES</code> <strong>réservés</strong> : aucun placement automatique, seulement <code>mld deploy <app> --node <alias></code>. Pas de Tailscale, pas de LAN vers les apps sœurs, pas de gardiens Ka. PM2 est chargé en <strong>LaunchDaemon système</strong> (<code>/Library/LaunchDaemons/pm2.<user>.plist</code>) parce que la session graphique n'est pas garantie au démarrage. Runtimes posés par <code>mld prepare</code> : Xcode CLT, Homebrew, node 26, pnpm, pm2, Python 3.13/3.14, uv ; cinq d'entre eux sont déjà raccordés au tunnel (wg1). Aucune application déployée au 10 septembre 2026 : <strong>12 machines prêtes</strong>.</p> | |
| 173 | +<h3>3a. Turquie — Macly <span class='cnt'>8 Macs</span></h3> | |
| 174 | +<p class='lead'>Turquie — Macly (AS199099 Erlion Bilişim), 8 Macs</p> | |
| 175 | +<table class='t hw'><thead><tr><th>Alias</th><th>Modèle (identifiant)</th><th>Puce (P + E)</th><th class=r>Cœurs</th><th class=r>RAM</th><th>SSD total / libre</th><th>N° de série</th><th>IP publique (LAN fournisseur)</th><th>Tunnel wg1</th><th>macOS</th><th>Ville</th><th>Ajouté</th><th>État</th></tr></thead><tbody><tr><td class="nw id">M1M64</td><td>Mac Studio (Mac13,1)</td><td>M1 Max (8 + 2)</td><td class="r nw">10</td><td class="r nw">64 Go</td><td class="nw">1 995 / 726 Go</td><td>WWVV29HYQW</td><td>45.74.241.221 (10.100.1.221)</td><td class="nw">—</td><td class="nw">26.6.2</td><td class="nw">Istanbul</td><td class="nw">2026-09-08</td><td class="nw">en ligne (2 j)</td></tr><tr><td class="nw id">m4me</td><td>Mac mini (Mac16,10)</td><td>M4 (4 + 6)</td><td class="r nw">10</td><td class="r nw">16 Go</td><td class="nw">245 / 201 Go</td><td>D46D7Q4XFL</td><td>45.74.245.36</td><td class="nw">—</td><td class="nw">26.6.2</td><td class="nw">Uşak</td><td class="nw">2026-09-08</td><td class="nw">en ligne (2 j)</td></tr><tr><td class="nw id">m4mf</td><td>Mac mini (Mac16,10)</td><td>M4 (4 + 6)</td><td class="r nw">10</td><td class="r nw">16 Go</td><td class="nw">245 / 202 Go</td><td>XY0YVKXV50</td><td>45.74.245.37 (10.100.2.37)</td><td class="nw">—</td><td class="nw">26.6.2</td><td class="nw">Uşak</td><td class="nw">2026-09-09</td><td class="nw">en ligne (1 j)</td></tr><tr><td class="nw id">m4mh</td><td>Mac mini (Mac16,10)</td><td>M4 (4 + 6)</td><td class="r nw">10</td><td class="r nw">16 Go</td><td class="nw">245 / 206 Go</td><td>H9FT6DP6V9</td><td>45.74.241.182 (10.100.1.182)</td><td class="nw">10.67.0.47</td><td class="nw">26.6.2</td><td class="nw">Istanbul</td><td class="nw">2026-09-10</td><td class="nw">en ligne (18 h)</td></tr><tr><td class="nw id">m1m16b</td><td>Mac mini (Macmini9,1 — vendu « M4 », c'est un M1)</td><td>M1 (4 + 4)</td><td class="r nw">8</td><td class="r nw">16 Go</td><td class="nw">245 / 188 Go</td><td>C07J70PHQ6NY</td><td>45.74.241.171</td><td class="nw">10.67.0.48</td><td class="nw">26.6.1</td><td class="nw">Istanbul</td><td class="nw">2026-09-10</td><td class="nw">en ligne (19 h)</td></tr><tr><td class="nw id">m4mi</td><td>Mac mini (Mac16,10)</td><td>M4 (4 + 6)</td><td class="r nw">10</td><td class="r nw">16 Go</td><td class="nw">245 / 204 Go</td><td>VW6R45M7HW</td><td>45.74.245.39</td><td class="nw">10.67.0.49</td><td class="nw">26.6.2</td><td class="nw">Uşak</td><td class="nw">2026-09-10</td><td class="nw">en ligne (16 h)</td></tr><tr><td class="nw id">m4mj</td><td>Mac mini (Mac16,10)</td><td>M4 (4 + 6)</td><td class="r nw">10</td><td class="r nw">16 Go</td><td class="nw">245 / 207 Go</td><td>QX4L7DQFJH</td><td>45.74.245.38</td><td class="nw">10.67.0.50</td><td class="nw">26.6.2</td><td class="nw">Uşak</td><td class="nw">2026-09-10</td><td class="nw">en ligne (16 h)</td></tr><tr><td class="nw id">m4mk</td><td>Mac mini (Mac16,10)</td><td>M4 (4 + 6)</td><td class="r nw">10</td><td class="r nw">16 Go</td><td class="nw">245 / 207 Go</td><td>DK65R60V95</td><td>45.74.241.98</td><td class="nw">10.67.0.51</td><td class="nw">26.6.2</td><td class="nw">Istanbul</td><td class="nw">2026-09-10</td><td class="nw">en ligne (12 h)</td></tr></tbody></table> | |
| 176 | +<div class='keep'><h3>3b. États-Unis et Irlande — MacStadium <span class='cnt'>3 Macs</span></h3> | |
| 177 | +<p class='lead'>États-Unis (Atlanta) et Irlande (Dublin) — MacStadium, 3 Macs</p> | |
| 178 | +<table class='t hw'><thead><tr><th>Alias</th><th>Modèle (identifiant)</th><th>Puce (P + E)</th><th class=r>Cœurs</th><th class=r>RAM</th><th>SSD total / libre</th><th>N° de série</th><th>IP publique</th><th>macOS</th><th>Ville</th><th>Ajouté</th><th>État</th></tr></thead><tbody><tr><td class="nw id">m2m16b</td><td>Mac mini (Mac14,12)</td><td>M2 Pro (6 + 4)</td><td class="r nw">10</td><td class="r nw">16 Go</td><td class="nw">995 / 942 Go</td><td>TQV5L6R9MK</td><td class="nw">208.52.154.68</td><td class="nw">26.6</td><td>Atlanta (AS395336)</td><td class="nw">2026-09-08</td><td class="nw">en ligne (3 j)</td></tr><tr><td class="nw id">m1m16</td><td>Mac mini (Macmini9,1)</td><td>M1 (4 + 4)</td><td class="r nw">8</td><td class="r nw">16 Go</td><td class="nw">995 / 958 Go</td><td>H2WGW18RQ6P0</td><td class="nw">207.254.29.71</td><td class="nw">26.6.2</td><td>Dublin (AS30377)</td><td class="nw">2026-09-08</td><td class="nw">en ligne (6 j)</td></tr><tr><td class="nw id">m2m16c</td><td>Mac mini (Mac14,3)</td><td>M2 (4 + 4)</td><td class="r nw">8</td><td class="r nw">16 Go</td><td class="nw">995 / 959 Go</td><td>X0KPDY0DQ5</td><td class="nw">207.254.29.128</td><td class="nw">26.6.2</td><td>Dublin (AS30377)</td><td class="nw">2026-09-08</td><td class="nw">en ligne (3 j)</td></tr></tbody></table></div> | |
| 179 | +<div class='keep'><h3>3c. Chypre — rentamac <span class='cnt'>1 Mac</span></h3> | |
| 180 | +<p class='lead'>Chypre (Limassol) — rentamac, 1 Mac</p> | |
| 181 | +<table class='t hw'><thead><tr><th>Alias</th><th>Modèle (identifiant)</th><th>Puce (P + E)</th><th class=r>Cœurs</th><th class=r>RAM</th><th>SSD total / libre</th><th>N° de série</th><th>Accès SSH</th><th>macOS</th><th>Ajouté</th><th>État</th></tr></thead><tbody><tr><td class="nw id">m4mg</td><td>Mac mini (Mac16,10)</td><td>M4 (4 + 6)</td><td class="r nw">10</td><td class="r nw">16 Go</td><td class="nw">245 / 203 Go</td><td>R7023K3WF0</td><td class="nw">gate1.rentamac.io:27335</td><td class="nw">26.6.2</td><td class="nw">2026-09-09</td><td class="nw">en ligne (1 j)</td></tr></tbody></table></div> | |
| 182 | +</section> | |
| 183 | + | |
| 184 | +<section class='sec'><div class='sec-h'><span class='num'>4</span><h2>Serveurs dédiés OVHcloud (Linux)</h2></div><p class=lead>Quatre serveurs bare-metal Ubuntu Server <strong>24.04.4 LTS</strong> (noyau 6.8), utilisateur <code>ubuntu</code> avec <strong>sudo sans mot de passe</strong>, root joignable par clé, fuseau America/Toronto, marqueur <code>/root/maclustr-postinstall.done</code>. Installés depuis le manager https://manager.ca.ovhcloud.com piloté en Safari/JXA (recette et scripts dans <code>cluster-skill/ovh-manager/</code>, ≈ 20 min par serveur). Ce ne sont <strong>pas</strong> des nœuds <code>mld</code>/agentd (pas de Homebrew ni de launchd) : on les pilote en SSH direct. Disques : 2 × 960 Go NVMe en RAID 1 logiciel partout (md2 <code>/boot</code>, md3 <code>/</code> 878 Go).</p> | |
| 185 | +<table class='t ovh'><thead><tr><th>Alias</th><th>Nom OVH (gamme, ID)</th><th>CPU</th><th class=r>RAM</th><th>Réseau</th><th>IPv4 / IPv6</th><th>Lieu</th><th>Rôle</th><th>Services actifs</th><th>État 10 sept. 22 h 35</th></tr></thead><tbody><tr><td class="id"><strong>BHS64</strong> (<code>ovh2</code>)</td><td><code>ns5044661.ip-51-161-112.net</code> (ADVANCE-2, ID 540625)</td><td>AMD EPYC 4345P, 8 c / 16 t, 3,8–5,5 GHz</td><td class="r nw">64 Go DDR5 ECC 3600</td><td>3 Gbit/s public, 25 Gbit/s privé</td><td><strong>51.161.112.61</strong> (gw 100.64.0.1) · <code>2607:5300:22a:3d00::/56</code></td><td>Beauharnois BHS8, baie BHS0805A03C, <code>ca-east-bhs-a</code></td><td><strong>passerelle principale MacLustr Tunnel</strong></td><td>WireGuard hub <code>wg0</code> 10.67.0.1 (udp 51820, 13 pairs), Caddy (36 routes, TLS auto), ufw actif, <code>tunnelctl</code></td><td>en ligne (22 h)</td></tr><tr><td class="id"><strong>BHS64b</strong> (<code>ovh3</code>)</td><td><code>ns5015175.ip-51-161-112.net</code> (ADVANCE-2)</td><td>AMD EPYC 4345P, 8 c / 16 t</td><td class="r nw">64 Go DDR5 ECC</td><td>3 Gbit/s</td><td><strong>51.161.112.66</strong> · <code>2607:5300:22a:4200::/56</code></td><td>Beauharnois, <code>ca-east-bhs-a</code></td><td><strong>réserve</strong> (usage à définir, pas une passerelle)</td><td>aucun (Caddy et ufw inactifs)</td><td>en ligne (3,5 h)</td></tr><tr><td class="id"><strong>BHS128</strong> (<code>ovh4</code>)</td><td><code>ns5044426.ip-51-161-112.net</code> (ADVANCE-1, ID 528691)</td><td>AMD EPYC 4245P, 6 c / 12 t, 3,9–5,4 GHz</td><td class="r nw">128 Go DDR5 3600</td><td>3 Gbit/s public, 25 Gbit/s privé</td><td><strong>51.161.112.69</strong> (gw 100.64.0.1) · <code>2607:5300:22a:4500::/56</code></td><td>Beauharnois BHS8, baie BHS0805A01C, <code>ca-east-bhs-a</code></td><td><strong>réserve</strong> (prêt à accueillir une app)</td><td>aucun (Caddy et ufw inactifs)</td><td>en ligne (installé ce soir 22 h 26)</td></tr><tr><td class="id"><strong>R9128</strong> (<code>ovh1</code>)</td><td><code>ns3049362.ip-51-255-75.eu</code> (RISE-L, ID 1907852)</td><td>AMD Ryzen 9 9950X, 16 c / 32 t, 4,3–5,7 GHz</td><td class="r nw">128 Go DDR5 3600</td><td>1 Gbit/s public + privé</td><td><strong>51.255.75.61</strong> (gw 51.255.75.254) · <code>2001:41d0:1004:273d::/64</code></td><td>Gravelines GRA1, baie G118A10, <code>eu-west-gra-a</code> (France)</td><td><strong>passerelle secondaire / secours</strong></td><td>WireGuard hub <code>wg0</code> 10.66.0.1, Caddy actif, ufw actif</td><td>en ligne (2 j)</td></tr></tbody></table> | |
| 186 | +<h3>Fiches</h3> | |
| 187 | +<div class='fiches'><p><strong>BHS64</strong> — installé le 10 septembre 2026 (hostname <code>bhs64</code>), puis <code>gateway-setup.sh BHS64 51.161.112.61 10.67.0</code>. Clé publique WireGuard du hub <code>QrpSXW8mIJjGMuB3eiP2LQJGpFgV6lTmBG9v0Cxsiz0=</code>. Les Macs s'y raccordent par leur interface <code>wg1</code> (10.67.0.x). Les certificats Let's Encrypt ont été copiés depuis R9128 pour un HTTPS immédiat. Latence ≈ 10 ms depuis Saint-Augustin. Outils : <code>tunnelctl</code> sur place, <code>mlt</code> sur le laptop (cible par défaut), <code>mld tunnel …</code> via M1M32. Toute route est repointée automatiquement à chaque <code>mld deploy</code>/<code>move</code>. Renouvellement automatique le 10 octobre 2026, sans engagement ; monitoring OVH avec intervention proactive.</p><p><strong>BHS64b</strong> — jumeau exact de BHS64, installé le 10 septembre en soirée (hostname <code>bhs64b</code>). Décision : aucun rôle pour l'instant et <strong>ce ne sera pas une passerelle tunnel</strong> ; réservé pour un autre usage.</p><p><strong>BHS128</strong> — ADVANCE-1 de 128 Go, installé le 10 septembre à 22 h 26 (hostname <code>bhs128</code>) ; 3 clés vérifiées depuis le laptop, M1M32 et M4M64a, sudo, fuseau et paquets de base OK (git, python3, tmux, jq, rsync, wireguard, ufw…). Aucun rôle : <strong>réserve prête à accueillir une app ou autre chose</strong>. Pour publier un site depuis ce serveur : le raccorder au hub BHS64 en pair WireGuard, puis <code>mlt add <domaine> BHS128:<port></code>.</p><p><strong>R9128</strong> — RISE-L installé le 8 septembre 2026 (hostname <code>r9128</code>). Première passerelle du tunnel (hub 10.66.0.1, Macs via <code>wg0</code>), devenue secours quand BHS64 a pris le relais (≈ +75 ms depuis le Québec). A hébergé SPB AI (API LLM llama.cpp CPU) le 9 septembre, retirée le jour même. Restent build-essential, cmake. Renouvellement automatique le 8 octobre 2026.</p></div> | |
| 188 | +</section> | |
| 189 | + | |
| 190 | +<section class='sec'><div class='sec-h'><span class='num'>5</span><h2>Topologie publique : tunnel et sites</h2></div><p class=lead>Les 36 domaines publics arrivent sur BHS64 (DNS <code>A → 51.161.112.61</code>) et sont relayés par WireGuard vers le nœud qui porte l'app. <code>mld</code> pose et repointe les routes ; <code>mld heal</code> répare pairs et routes manquants.</p> | |
| 191 | +<table class='t tun'><col style='width:22%'><col style='width:78%'><thead><tr><th>Nœud (wg1)</th><th>Sites publics (→ port local)</th></tr></thead><tbody><tr><td class="id">M3U96a (10.67.0.10)</td><td>www.administration-ka.com (3300), www.fetcha.co (8220), www.job-ka.com (8096), www.polyllm.io (8240), www.qc26.ca (8200) + redirection qc26.ca, www.rareindex.io (8210), www.sorti-ka.com (8120), www.uqo-imm1033.app (8161), www.vrai-prix.com (8090)</td></tr><tr><td class="id">M3U96b (10.67.0.11)</td><td>www.hfmarketdata.io (8090)</td></tr><tr><td class="id">M2U64 (10.67.0.12)</td><td>app.maclustr.io (8787), cloud.spboucher.ai (5001), console.maclustr.io (3000), node.maclustr.io (8003), www.food-ka.com (8097), www.spboucher.ai (3100), www.uqo-eval.app (8150), www.vquant.ai (5000)</td></tr><tr><td class="id">M4M64a (10.67.0.13)</td><td>www.fabri-ka.com (8097), www.ka-stats.com (8140), www.lou-ka.com (8095), www.maclustr.io (8280), www.pdb-api.co (8270), www.uqo-chat.app (8190)</td></tr><tr><td class="id">M4M64b (10.67.0.14)</td><td>www.api-ka.com (8000), www.auto-ka.com (8095), www.cancerindex.io (8250), www.groupe-ka.com (8110), www.immo-ka.com (8096), www.uqo-imm1003.app (8160), www.websensor.io (8260)</td></tr><tr><td class="id">M4M36 (10.67.0.17)</td><td>www.ka2.bot (8799), www.ka4.bot (8899), www.ka6.bot (8999)</td></tr><tr><td class="id">M1M32 (10.67.0.2)</td><td>git.spboucher.ai (7420)</td></tr><tr><td class="id">M2M32 (10.67.0.18), m4mh (.47), m1m16b (.48), m4mi (.49), m4mj (.50), m4mk (.51)</td><td>raccordés au tunnel, aucune route (prêts à publier)</td></tr></tbody></table> | |
| 192 | +<div class='three-col'> | |
| 193 | + <div class='panel'><h4>Chemin d'une requête</h4> | |
| 194 | + <ol class='flow'><li><div><b>DNS</b><span>A → 51.161.112.61 (BHS64, Beauharnois) ; secours 51.255.75.61 (R9128, Gravelines)</span></div></li><li><div><b>Caddy</b><span>termine le TLS Let’s Encrypt et choisit l'upstream d'après l'hôte</span></div></li><li><div><b>WireGuard</b><span>hub 10.67.0.1 → interface wg1 du nœud (10.67.0.x), MTU 1380, keepalive 25 s</span></div></li><li><div><b>Nœud</b><span>processus PM2 ou launchd sur le port local ; healthcheck local puis public à chaque déploiement</span></div></li></ol> | |
| 195 | + </div> | |
| 196 | + <div class='panel'><h4>Passerelles live</h4> | |
| 197 | + <div class='gw'><b>BHS64</b><span>principale · Beauharnois (Québec)</span><span class='mono'>36 routes · 14/14 pairs · 51,4 Go reçus</span></div><div class='gw'><b>R9128</b><span>secondaire · Gravelines (France)</span><span class='mono'>2 routes · 3/3 pairs · 0,4 Go reçus</span></div> | |
| 198 | + </div> | |
| 199 | + <div class='panel'><h4>Services internes</h4><p class='small' style='color:var(--ink-2)'>maclustr-agentd (M4M64a:9210, API Bearer, routes <code>/api/cluster</code>, <code>/api/apps</code>, <code>/api/tunnel</code>, <code>/api/events</code>) alimente les apps macOS MacLustr 3.0.7 et iOS 4.2.0 ; Postgres 17 local sur M3U96a, M2U64, M4M64a, M4M64b et M1M32 ; Redis sur M3U96a, M3U96b, M4M64b ; Docker sur les Studios et M2M32/M2M32b.</p></div> | |
| 200 | +</div> | |
| 201 | +</section> | |
| 202 | + | |
| 203 | +<section class='sec'><div class='sec-h'><span class='num'>6</span><h2>Emplacements et carte</h2></div><p class=lead>7 sites dans 6 pays sur trois continents. Le LAN de Saint-Augustin porte le cœur du cluster ; Beauharnois et Gravelines publient les sites ; Istanbul, Atlanta, Dublin et Limassol étendent la capacité. Géolocalisation vérifiée par IP et latence.</p> | |
| 204 | +<figure class='map'><img src='map.png' alt='Carte mondiale des nœuds'><figcaption>Vue d'ensemble : épingles dimensionnées selon le nombre de nœuds, arcs WireGuard vers les passerelles OVHcloud. Capture du site www.maclustr.io/map.</figcaption></figure> | |
| 205 | +<div class='sites'><div class='site'><div class='s-h'><b>Saint-Augustin-de-Desmaures</b><span class='mono'>21 nœuds</span></div><div class='s-r'>Québec, Canada · MacLustr (LAN privé) · <span class='mono'>46.74, -71.46</span></div><p>Le cœur du cluster : 21 Macs sur un réseau local 10 GbE, deux NAS UGREEN.</p><div class='s-n'><span>M3U96a</span><span>M3U96b</span><span>M2U64</span><span>M4M64a</span><span>M4M64b</span><span>M4BP48</span><span>M4BP36</span><span>M4M36</span><span>M2M32</span><span>M2M32b</span><span>M2M32c</span><span>m4mc</span><span>M1M32</span><span>m4ma</span><span>m4mb</span><span>m4md</span><span>m2m16</span><span>M3BA24</span><span>M3BA16</span><span>m2m8a</span><span>m2m8b</span></div></div><div class='site'><div class='s-h'><b>Istanbul</b><span class='mono'>8 nœuds</span></div><div class='s-r'>Turquie · Macly (réseau Erlion) · <span class='mono'>41.01, 28.98</span></div><p>Huit Macs dédiés loués : un Mac Studio M1 Max 64 Go et sept Mac mini.</p><div class='s-n'><span>M1M64</span><span>m4me</span><span>m4mf</span><span>m4mh</span><span>m1m16b</span><span>m4mi</span><span>m4mj</span><span>m4mk</span></div></div><div class='site'><div class='s-h'><b>Beauharnois</b><span class='mono'>3 nœuds</span></div><div class='s-r'>Québec, Canada · OVHcloud BHS8 · <span class='mono'>45.31, -73.87</span></div><p>Passerelle principale du MacLustr Tunnel et deux serveurs de réserve. ≈ 10 ms du LAN.</p><div class='s-n'><span>BHS64</span><span>BHS64b</span><span>BHS128</span></div></div><div class='site'><div class='s-h'><b>Dublin</b><span class='mono'>2 nœuds</span></div><div class='s-r'>Irlande · MacStadium · <span class='mono'>53.34, -6.25</span></div><p>Deux Mac mini dédiés (M1, M2).</p><div class='s-n'><span>m1m16</span><span>m2m16c</span></div></div><div class='site'><div class='s-h'><b>Gravelines</b><span class='mono'>1 nœud</span></div><div class='s-r'>Hauts-de-France, France · OVHcloud GRA1 · <span class='mono'>50.99, 2.13</span></div><p>Passerelle secondaire : secours et présence européenne.</p><div class='s-n'><span>R9128</span></div></div><div class='site'><div class='s-h'><b>Atlanta</b><span class='mono'>1 nœud</span></div><div class='s-r'>Géorgie, États-Unis · MacStadium · <span class='mono'>33.79, -84.41</span></div><p>Mac mini M2 Pro dédié.</p><div class='s-n'><span>m2m16b</span></div></div><div class='site'><div class='s-h'><b>Limassol</b><span class='mono'>1 nœud</span></div><div class='s-r'>Chypre · rentamac · <span class='mono'>34.69, 33.04</span></div><p>Mac mini M4 derrière NAT, joint par la passerelle SSH du fournisseur.</p><div class='s-n'><span>m4mg</span></div></div></div> | |
| 206 | +</section> | |
| 207 | + | |
| 208 | +<section class='sec'><div class='sec-h'><span class='num'>7</span><h2>Répartitions et capacité</h2></div> | |
| 209 | +<h3>7a. Par emplacement</h3> | |
| 210 | +<table class='t bars'><col style='width:24%'><col style='width:6%'><col style='width:26%'><col style='width:22%'><col><thead><tr><th>Emplacement</th><th class='r'>Nœuds</th><th>Cœurs / threads</th><th>Mémoire</th><th>Alias</th></tr></thead><tbody><tr><td class='id'>Saint-Augustin-de-Desmaures<br><small>Québec, Canada · MacLustr (LAN privé)</small></td><td class='r'>21</td><td class='meter wide'><span class='bar '><i style='width:100%'></i></span><span class='v'>294 cœurs</span></td><td class='meter wide'><span class='bar alt'><i style='width:100%'></i></span><span class='v'>784 Go</span></td><td class='mono small'>M3U96a, M3U96b, M2U64, M4M64a, M4M64b, M4BP48, M4BP36, M4M36, M2M32, M2M32b, M2M32c, m4mc, M1M32, m4ma, m4mb, m4md, m2m16, M3BA24, M3BA16, m2m8a, m2m8b</td></tr><tr><td class='id'>Istanbul<br><small>Turquie · Macly (réseau Erlion)</small></td><td class='r'>8</td><td class='meter wide'><span class='bar '><i style='width:26.53061224489796%'></i></span><span class='v'>78 cœurs</span></td><td class='meter wide'><span class='bar alt'><i style='width:22.448979591836736%'></i></span><span class='v'>176 Go</span></td><td class='mono small'>M1M64, m4me, m4mf, m4mh, m1m16b, m4mi, m4mj, m4mk</td></tr><tr><td class='id'>Beauharnois<br><small>Québec, Canada · OVHcloud BHS8</small></td><td class='r'>3</td><td class='meter wide'><span class='bar '><i style='width:14.965986394557824%'></i></span><span class='v'>44 thr</span></td><td class='meter wide'><span class='bar alt'><i style='width:32.6530612244898%'></i></span><span class='v'>256 Go</span></td><td class='mono small'>BHS64, BHS64b, BHS128</td></tr><tr><td class='id'>Dublin<br><small>Irlande · MacStadium</small></td><td class='r'>2</td><td class='meter wide'><span class='bar '><i style='width:5.442176870748299%'></i></span><span class='v'>16 cœurs</span></td><td class='meter wide'><span class='bar alt'><i style='width:4.081632653061225%'></i></span><span class='v'>32 Go</span></td><td class='mono small'>m1m16, m2m16c</td></tr><tr><td class='id'>Gravelines<br><small>Hauts-de-France, France · OVHcloud GRA1</small></td><td class='r'>1</td><td class='meter wide'><span class='bar '><i style='width:10.884353741496598%'></i></span><span class='v'>32 thr</span></td><td class='meter wide'><span class='bar alt'><i style='width:16.3265306122449%'></i></span><span class='v'>128 Go</span></td><td class='mono small'>R9128</td></tr><tr><td class='id'>Atlanta<br><small>Géorgie, États-Unis · MacStadium</small></td><td class='r'>1</td><td class='meter wide'><span class='bar '><i style='width:3.4013605442176873%'></i></span><span class='v'>10 cœurs</span></td><td class='meter wide'><span class='bar alt'><i style='width:2.0408163265306123%'></i></span><span class='v'>16 Go</span></td><td class='mono small'>m2m16b</td></tr><tr><td class='id'>Limassol<br><small>Chypre · rentamac</small></td><td class='r'>1</td><td class='meter wide'><span class='bar '><i style='width:3.4013605442176873%'></i></span><span class='v'>10 cœurs</span></td><td class='meter wide'><span class='bar alt'><i style='width:2.0408163265306123%'></i></span><span class='v'>16 Go</span></td><td class='mono small'>m4mg</td></tr></tbody></table> | |
| 211 | +<h3>7b. Par modèle et par puce</h3><table class='t models'><thead><tr><th>Famille</th><th class=r>Local</th><th class=r>Loués</th><th class=r>OVH</th><th class=r>Total</th></tr></thead><tbody><tr><td class="id">Mac Studio</td><td class="r nw">11</td><td class="r nw">1</td><td class="r nw">—</td><td class="r nw">12</td></tr><tr><td class="id">Mac mini</td><td class="r nw">6</td><td class="r nw">11</td><td class="r nw">—</td><td class="r nw">17</td></tr><tr><td class="id">MacBook Pro</td><td class="r nw">2</td><td class="r nw">—</td><td class="r nw">—</td><td class="r nw">2</td></tr><tr><td class="id">MacBook Air</td><td class="r nw">2</td><td class="r nw">—</td><td class="r nw">—</td><td class="r nw">2</td></tr><tr><td class="id">Serveur x86 (AMD EPYC 4345P ×2, EPYC 4245P, Ryzen 9 9950X)</td><td class="r nw">—</td><td class="r nw">—</td><td class="r nw">4</td><td class="r nw">4</td></tr><tr><td class="id">Puces Apple : M4 (Max ×5, Pro ×1, base ×10) 16 · M3 (Ultra ×2, base ×2) 4 · M2 (Ultra ×1, Max ×3, Pro ×2, base ×3) 9 · M1 (Max ×2, base ×2) 4</td><td class="r nw"></td><td class="r nw"></td><td class="r nw"></td><td class="r nw">33</td></tr></tbody></table> | |
| 212 | +<div class='keep'><h3>7c. Capacité libre</h3><p class='lead'>Capacité libre</p> | |
| 213 | +<div class='kpis three'><div class='kpi'><b>14</b><span>nœuds LAN libres</span><small>150 cœurs · 332 Go</small></div><div class='kpi'><b>12</b><span>Macs loués libres</span><small>114 cœurs · 240 Go</small></div><div class='kpi'><b>2</b><span>serveurs OVH libres</span><small>BHS64b 64 Go · BHS128 128 Go</small></div></div></div> | |
| 214 | +</section> | |
| 215 | + | |
| 216 | +<section class='sec'><div class='sec-h'><span class='num'>8</span><h2>Applications hébergées</h2></div><p class=lead>36 applications, toutes décrites par un manifeste mld (dir, processus, runtimes, RAM, ports, placement, tunnel). Le registre M1M32:~/dispatch/registry.json est la seule source de vérité de « quelle app tourne où » ; état confirmé par maclustr-agentd au moment de la génération.</p> | |
| 217 | +<div class='app-group'><h3>Groupe Ka <span class='cnt'>15</span></h3><p class='lead'>L'écosystème immobilier, automobile, alimentaire et emploi du Québec : une famille de plateformes qui partagent identité (KA ID), données (API·Ka) et gardiens autonomes.</p><table class='t apps'><col style='width:15%'><col style='width:19%'><col style='width:11%'><col><col style='width:15%'><thead><tr><th>Application</th><th>Domaine</th><th>Nœud</th><th>Description</th><th>État</th></tr></thead><tbody><tr><td class='id'>Groupe Ka</td><td class='mono'>www.groupe-ka.com</td><td class='mono nw'>M4M64b<span class='faint'>:8110</span></td><td>Portail du groupe : vision, univers, dossier investisseurs.</td><td class='nw'><span class='st on'>en ligne</span> <span class='faint mono small'>77 ms · 94 % / 24 h</span></td></tr><tr><td class='id'>Lou·Ka</td><td class='mono'>www.lou-ka.com</td><td class='mono nw'>M4M64a<span class='faint'>:8095</span></td><td>Location résidentielle : annonces, carte 3D, Lou-Ka Score.</td><td class='nw'><span class='st on'>en ligne</span> <span class='faint mono small'>350 ms · 100 % / 24 h</span></td></tr><tr><td class='id'>Immo·Ka</td><td class='mono'>www.immo-ka.com</td><td class='mono nw'>M4M64b<span class='faint'>:8096</span></td><td>Propriétés à vendre : 70 000+ annonces, analyses de prix, KA Scores.</td><td class='nw'><span class='st on'>en ligne</span> <span class='faint mono small'>66 ms · 94 % / 24 h</span></td></tr><tr><td class='id'>Vrai-Prix</td><td class='mono'>www.vrai-prix.com</td><td class='mono nw'>M3U96a<span class='faint'>:8090</span></td><td>Évaluation hédonique, méthode du coût, statistiques de marché.</td><td class='nw'><span class='st on'>en ligne</span> <span class='faint mono small'>60 ms · 90 % / 24 h</span></td></tr><tr><td class='id'>Auto·Ka</td><td class='mono'>www.auto-ka.com</td><td class='mono nw'>M4M64b<span class='faint'>:8095</span></td><td>Marché automobile : véhicules, prix, fiches v3.</td><td class='nw'><span class='st on'>en ligne</span> <span class='faint mono small'>300 ms · 94 % / 24 h</span></td></tr><tr><td class='id'>Fabri·Ka</td><td class='mono'>www.fabri-ka.com</td><td class='mono nw'>M4M64a<span class='faint'>:8097</span></td><td>Artisans, fabricants et ateliers du Québec.</td><td class='nw'><span class='st on'>en ligne</span> <span class='faint mono small'>39 ms · 100 % / 24 h</span></td></tr><tr><td class='id'>Food·Ka</td><td class='mono'>www.food-ka.com</td><td class='mono nw'>M2U64<span class='faint'>:8097</span></td><td>Alimentation : produits, commerces, prix.</td><td class='nw'><span class='st on'>en ligne</span> <span class='faint mono small'>134 ms · 93 % / 24 h</span></td></tr><tr><td class='id'>Sorti·Ka</td><td class='mono'>www.sorti-ka.com</td><td class='mono nw'>M3U96a<span class='faint'>:8120</span></td><td>Sorties, événements et activités.</td><td class='nw'><span class='st on'>en ligne</span> <span class='faint mono small'>158 ms · 90 % / 24 h</span></td></tr><tr><td class='id'>Job·Ka</td><td class='mono'>www.job-ka.com</td><td class='mono nw'>M3U96a<span class='faint'>:8096</span></td><td>Emplois et carrières.</td><td class='nw'><span class='st on'>en ligne</span> <span class='faint mono small'>146 ms · 90 % / 24 h</span></td></tr><tr><td class='id'>API·Ka</td><td class='mono'>www.api-ka.com</td><td class='mono nw'>M4M64b<span class='faint'>:8000</span></td><td>API unifiée du groupe : Postgres 17, Redis, 24 outils agent.</td><td class='nw'><span class='st on'>en ligne</span> <span class='faint mono small'>79 ms · 94 % / 24 h</span></td></tr><tr><td class='id'>Ka·Stats</td><td class='mono'>www.ka-stats.com</td><td class='mono nw'>M4M64a<span class='faint'>:8140</span></td><td>Observatoire statistique des univers Ka.</td><td class='nw'><span class='st on'>en ligne</span> <span class='faint mono small'>47 ms · 100 % / 24 h</span></td></tr><tr><td class='id'>Administration·Ka</td><td class='mono'>www.administration-ka.com</td><td class='mono nw'>M3U96a<span class='faint'>:3300</span></td><td>Console d'administration multi-sites, pilotée par le registre mld.</td><td class='nw'><span class='st on'>en ligne</span> <span class='faint mono small'>44 ms · 90 % / 24 h</span></td></tr><tr><td class='id'>Gardien ka2</td><td class='mono'>www.ka2.bot</td><td class='mono nw'>M4M36<span class='faint'>:8799</span></td><td>Agent gardien autonome : surveillance et réparation.</td><td class='nw'><span class='st on'>en ligne</span> <span class='faint mono small'>74 ms · 94 % / 24 h</span></td></tr><tr><td class='id'>Gardien ka4</td><td class='mono'>www.ka4.bot</td><td class='mono nw'>M4M36<span class='faint'>:8899</span></td><td>Agent gardien autonome.</td><td class='nw'><span class='st on'>en ligne</span> <span class='faint mono small'>73 ms · 94 % / 24 h</span></td></tr><tr><td class='id'>Gardien ka6</td><td class='mono'>www.ka6.bot</td><td class='mono nw'>M4M36<span class='faint'>:8999</span></td><td>Agent gardien autonome.</td><td class='nw'><span class='st on'>en ligne</span> <span class='faint mono small'>76 ms · 94 % / 24 h</span></td></tr></tbody></table></div><div class='app-group'><h3>Données & intelligence <span class='cnt'>9</span></h3><p class='lead'>Indices, marchés et infrastructures de données servis en API publiques.</p><table class='t apps'><col style='width:15%'><col style='width:19%'><col style='width:11%'><col><col style='width:15%'><thead><tr><th>Application</th><th>Domaine</th><th>Nœud</th><th>Description</th><th>État</th></tr></thead><tbody><tr><td class='id'>HF Market Data</td><td class='mono'>www.hfmarketdata.io</td><td class='mono nw'>M3U96b<span class='faint'>:8090</span></td><td>Données de marché haute fréquence : actions, futures, options, fondamentaux SEC. API + MCP.</td><td class='nw'><span class='st on'>en ligne</span> <span class='faint mono small'>79 ms · 94 % / 24 h</span></td></tr><tr><td class='id'>RareIndex</td><td class='mono'>www.rareindex.io</td><td class='mono nw'>M3U96a<span class='faint'>:8210</span></td><td>Intelligence de marché des objets de collection : 212 connecteurs.</td><td class='nw'><span class='st on'>en ligne</span> <span class='faint mono small'>49 ms · 90 % / 24 h</span></td></tr><tr><td class='id'>CancerIndex</td><td class='mono'>www.cancerindex.io</td><td class='mono nw'>M4M64b<span class='faint'>:8250</span></td><td>Index mondial des cancers : 9 510 entités NCIt, 13 connecteurs, pgvector.</td><td class='nw'><span class='st on'>en ligne</span> <span class='faint mono small'>49 ms · 94 % / 24 h</span></td></tr><tr><td class='id'>WebSensor</td><td class='mono'>www.websensor.io</td><td class='mono nw'>M4M64b<span class='faint'>:8260</span></td><td>Détection de changements web : 4 000+ capteurs, 16 connecteurs.</td><td class='nw'><span class='st on'>en ligne</span> <span class='faint mono small'>42 ms · 94 % / 24 h</span></td></tr><tr><td class='id'>Fetcha</td><td class='mono'>www.fetcha.co</td><td class='mono nw'>M3U96a<span class='faint'>:8220</span></td><td>Infrastructure d'accès web intelligent : scrape, crawl, map, SDK.</td><td class='nw'><span class='st on'>en ligne</span> <span class='faint mono small'>48 ms · 90 % / 24 h</span></td></tr><tr><td class='id'>PolyLLM</td><td class='mono'>www.polyllm.io</td><td class='mono nw'>M3U96a<span class='faint'>:8240</span></td><td>Une interface, tous les modèles : 9 fournisseurs, Arena, BYOK.</td><td class='nw'><span class='st on'>en ligne</span> <span class='faint mono small'>46 ms · 90 % / 24 h</span></td></tr><tr><td class='id'>vQuant</td><td class='mono'>www.vquant.ai</td><td class='mono nw'>M2U64<span class='faint'>:5000</span></td><td>Terminal quantitatif : indices, portefeuilles, MFA.</td><td class='nw'><span class='st on'>en ligne</span> <span class='faint mono small'>79 ms · 93 % / 24 h</span></td></tr><tr><td class='id'>QC26</td><td class='mono'>www.qc26.ca</td><td class='mono nw'>M3U96a<span class='faint'>:8200</span></td><td>L'élection québécoise 2026 en données : sondages, Monte Carlo, revue de presse.</td><td class='nw'><span class='st on'>en ligne</span> <span class='faint mono small'>58 ms · 89 % / 24 h</span></td></tr><tr><td class='id'>PDB API</td><td class='mono'>www.pdb-api.co</td><td class='mono nw'>M4M64a<span class='faint'>:8270</span></td><td>Explorateur et API de la base SPID (FastAPI + DuckDB).</td><td class='nw'><span class='st on'>en ligne</span> <span class='faint mono small'>40 ms · 100 % / 24 h</span></td></tr></tbody></table></div><div class='app-group keep'><h3>Enseignement UQO <span class='cnt'>4</span></h3><p class='lead'>Outils de cours interactifs, tuteur IA et ateliers d'évaluation immobilière.</p><table class='t apps'><col style='width:15%'><col style='width:19%'><col style='width:11%'><col><col style='width:15%'><thead><tr><th>Application</th><th>Domaine</th><th>Nœud</th><th>Description</th><th>État</th></tr></thead><tbody><tr><td class='id'>UQO Éval</td><td class='mono'>www.uqo-eval.app</td><td class='mono nw'>M2U64<span class='faint'>:8150</span></td><td>Atelier d'évaluation immobilière : hédonique, coût, à vendre, marché.</td><td class='nw'><span class='st on'>en ligne</span> <span class='faint mono small'>47 ms · 93 % / 24 h</span></td></tr><tr><td class='id'>UQO IMM1003</td><td class='mono'>www.uqo-imm1003.app</td><td class='mono nw'>M4M64b<span class='faint'>:8160</span></td><td>Notes de cours interactives et séances vidéo.</td><td class='nw'><span class='st on'>en ligne</span> <span class='faint mono small'>48 ms · 94 % / 24 h</span></td></tr><tr><td class='id'>UQO IMM1033</td><td class='mono'>www.uqo-imm1033.app</td><td class='mono nw'>M3U96a<span class='faint'>:8161</span></td><td>Notes de cours interactives et séances vidéo.</td><td class='nw'><span class='st on'>en ligne</span> <span class='faint mono small'>61 ms · 90 % / 24 h</span></td></tr><tr><td class='id'>UQO-Chat</td><td class='mono'>www.uqo-chat.app</td><td class='mono nw'>M4M64a<span class='faint'>:8190</span></td><td>Tuteur IA des cours IMM1003 et IMM1033, sur invitation.</td><td class='nw'><span class='st on'>en ligne</span> <span class='faint mono small'>52 ms · 100 % / 24 h</span></td></tr></tbody></table></div><div class='app-group keep'><h3>Plateforme MacLustr <span class='cnt'>6</span></h3><p class='lead'>Les services qui font tourner le cluster lui-même.</p><table class='t apps'><col style='width:15%'><col style='width:19%'><col style='width:11%'><col><col style='width:15%'><thead><tr><th>Application</th><th>Domaine</th><th>Nœud</th><th>Description</th><th>État</th></tr></thead><tbody><tr><td class='id'>www.maclustr.io</td><td class='mono'>www.maclustr.io</td><td class='mono nw'>M4M64a<span class='faint'>:8280</span></td><td>Cette page : vitrine du cluster, inventaire et état live des nœuds et des applications.</td><td class='nw'><span class='st on'>en ligne</span> <span class='faint mono small'>46 ms · 99 % / 24 h</span></td></tr><tr><td class='id'>maclustr-agentd</td><td class='mono'>service interne</td><td class='mono nw'>M4M64a<span class='faint'>:9210</span></td><td>Agent de supervision : métriques des 33 nœuds, santé des apps, état du tunnel. Alimente les apps macOS et iOS MacLustr et ce site.</td><td class='nw'><span class='st on'>en ligne</span> <span class='faint mono small'>100 % / 24 h</span></td></tr><tr><td class='id'>Console MacLustr</td><td class='mono'>console.maclustr.io</td><td class='mono nw'>M2U64<span class='faint'>:3000</span></td><td>Console web du cluster.</td><td class='nw'><span class='st on'>en ligne</span> <span class='faint mono small'>74 ms · 93 % / 24 h</span></td></tr><tr><td class='id'>App Manager</td><td class='mono'>app.maclustr.io</td><td class='mono nw'>M2U64<span class='faint'>:8787</span></td><td>Gestionnaire d'applications.</td><td class='nw'><span class='st on'>en ligne</span> <span class='faint mono small'>78 ms · 93 % / 24 h</span></td></tr><tr><td class='id'>Node Pages</td><td class='mono'>node.maclustr.io</td><td class='mono nw'>M2U64<span class='faint'>:8003</span></td><td>Pages de bord par nœud.</td><td class='nw'><span class='st on'>en ligne</span> <span class='faint mono small'>68 ms · 93 % / 24 h</span></td></tr><tr><td class='id'>spbgit</td><td class='mono'>git.spboucher.ai</td><td class='mono nw'>M1M32<span class='faint'>:7420</span></td><td>Forge git privée : dépôts bare de toutes les apps, source de vérité du code.</td><td class='nw'><span class='st on'>en ligne</span> <span class='faint mono small'>94 ms · 94 % / 24 h</span></td></tr></tbody></table></div><div class='app-group keep'><h3>SPB <span class='cnt'>2</span></h3><p class='lead'>Sites et services personnels.</p><table class='t apps'><col style='width:15%'><col style='width:19%'><col style='width:11%'><col><col style='width:15%'><thead><tr><th>Application</th><th>Domaine</th><th>Nœud</th><th>Description</th><th>État</th></tr></thead><tbody><tr><td class='id'>spboucher.ai</td><td class='mono'>www.spboucher.ai</td><td class='mono nw'>M2U64<span class='faint'>:3100</span></td><td>Site personnel.</td><td class='nw'><span class='st on'>en ligne</span> <span class='faint mono small'>76 ms · 93 % / 24 h</span></td></tr><tr><td class='id'>SPB Cloud</td><td class='mono'>cloud.spboucher.ai</td><td class='mono nw'>M2U64<span class='faint'>:5001</span></td><td>Nuage privé : fichiers, partage, OTP, réorganisation IA.</td><td class='nw'><span class='st on'>en ligne</span> <span class='faint mono small'>76 ms · 93 % / 24 h</span></td></tr></tbody></table></div> | |
| 218 | +</section> | |
| 219 | + | |
| 220 | +<section class='sec'><div class='sec-h'><span class='num'>9</span><h2>Instantané live</h2></div><p class=lead>Mesures de maclustr-agentd le 10 septembre 2026 à 23 h 09 : 32/33 Macs en ligne, CPU moyen 13,6 %, 296 Go de mémoire utilisés sur 1 008 Go en ligne, 20 870 processus, 36/37 applications saines.</p> | |
| 221 | +<table class='t heat'><col style='width:9%'><col style='width:9%'><col style='width:5%'><col style='width:6%'><col style='width:9%'><col style='width:15%'><col style='width:19%'><col style='width:19%'><col style='width:4%'><col><thead><tr><th>Nœud</th><th>Puce</th><th class='r'>Cœurs</th><th class='r'>RAM</th><th>État</th><th>CPU</th><th>Mémoire</th><th>Disque</th><th class='r'>Load</th><th>macOS</th></tr></thead><tbody><tr><td class='id'>M3U96a</td><td>M3 Ultra</td><td class='r'>32</td><td class='r'>96 Go</td><td><span class='st on'>en ligne</span></td><td class='meter'><span class='bar '><i style='width:15.2%'></i></span><span class='v'>15 %</span></td><td class='meter'><span class='bar '><i style='width:17.8125%'></i></span><span class='v'>17,1 / 96 Go</span></td><td class='meter'><span class='bar '><i style='width:1.187904967602592%'></i></span><span class='v'>11 / 926 Go</span></td><td class='r mono'>3.11</td><td class='mono'>26.6</td></tr><tr><td class='id'>M3U96b</td><td>M3 Ultra</td><td class='r'>32</td><td class='r'>96 Go</td><td><span class='st on'>en ligne</span></td><td class='meter'><span class='bar '><i style='width:10.3%'></i></span><span class='v'>10 %</span></td><td class='meter'><span class='bar '><i style='width:16.5625%'></i></span><span class='v'>15,9 / 96 Go</span></td><td class='meter'><span class='bar '><i style='width:1.187904967602592%'></i></span><span class='v'>11 / 926 Go</span></td><td class='r mono'>1.91</td><td class='mono'>26.4</td></tr><tr><td class='id'>M2U64</td><td>M2 Ultra</td><td class='r'>24</td><td class='r'>64 Go</td><td><span class='st on'>en ligne</span></td><td class='meter'><span class='bar '><i style='width:5.4%'></i></span><span class='v'>5 %</span></td><td class='meter'><span class='bar '><i style='width:17.34375%'></i></span><span class='v'>11,1 / 64 Go</span></td><td class='meter'><span class='bar '><i style='width:1.187904967602592%'></i></span><span class='v'>11 / 926 Go</span></td><td class='r mono'>1.16</td><td class='mono'>26.5</td></tr><tr><td class='id'>M4M64a</td><td>M4 Max</td><td class='r'>16</td><td class='r'>64 Go</td><td><span class='st on'>en ligne</span></td><td class='meter'><span class='bar '><i style='width:25.2%'></i></span><span class='v'>25 %</span></td><td class='meter'><span class='bar '><i style='width:20.9375%'></i></span><span class='v'>13,4 / 64 Go</span></td><td class='meter'><span class='bar '><i style='width:1.187904967602592%'></i></span><span class='v'>11 / 926 Go</span></td><td class='r mono'>1.61</td><td class='mono'>26.5.1</td></tr><tr><td class='id'>M4M64b</td><td>M4 Max</td><td class='r'>16</td><td class='r'>64 Go</td><td><span class='st on'>en ligne</span></td><td class='meter'><span class='bar '><i style='width:14.9%'></i></span><span class='v'>15 %</span></td><td class='meter'><span class='bar '><i style='width:22.03125%'></i></span><span class='v'>14,1 / 64 Go</span></td><td class='meter'><span class='bar '><i style='width:1.187904967602592%'></i></span><span class='v'>11 / 926 Go</span></td><td class='r mono'>3.06</td><td class='mono'>26.5.1</td></tr><tr><td class='id'>M4BP48</td><td>M4 Max</td><td class='r'>16</td><td class='r'>48 Go</td><td><span class='st on'>en ligne</span></td><td class='meter'><span class='bar '><i style='width:7.5%'></i></span><span class='v'>8 %</span></td><td class='meter'><span class='bar '><i style='width:25.208333333333332%'></i></span><span class='v'>12,1 / 48 Go</span></td><td class='meter'><span class='bar '><i style='width:1.2958963282937366%'></i></span><span class='v'>12 / 926 Go</span></td><td class='r mono'>1.49</td><td class='mono'>26.5.1</td></tr><tr><td class='id'>M4BP36</td><td>M4 Max</td><td class='r'>14</td><td class='r'>36 Go</td><td><span class='st on'>en ligne</span></td><td class='meter'><span class='bar '><i style='width:16.7%'></i></span><span class='v'>17 %</span></td><td class='meter'><span class='bar '><i style='width:27.77777777777778%'></i></span><span class='v'>10,0 / 36 Go</span></td><td class='meter'><span class='bar '><i style='width:1.187904967602592%'></i></span><span class='v'>11 / 926 Go</span></td><td class='r mono'>2.33</td><td class='mono'>26.5.1</td></tr><tr><td class='id'>M4M36</td><td>M4 Max</td><td class='r'>14</td><td class='r'>36 Go</td><td><span class='st on'>en ligne</span></td><td class='meter'><span class='bar '><i style='width:9.6%'></i></span><span class='v'>10 %</span></td><td class='meter'><span class='bar '><i style='width:23.055555555555557%'></i></span><span class='v'>8,3 / 36 Go</span></td><td class='meter'><span class='bar '><i style='width:2.391304347826087%'></i></span><span class='v'>11 / 460 Go</span></td><td class='r mono'>1.44</td><td class='mono'>26.5.2</td></tr><tr><td class='id'>M2M32</td><td>M2 Max</td><td class='r'>12</td><td class='r'>32 Go</td><td><span class='st on'>en ligne</span></td><td class='meter'><span class='bar '><i style='width:10.4%'></i></span><span class='v'>10 %</span></td><td class='meter'><span class='bar '><i style='width:24.0625%'></i></span><span class='v'>7,7 / 32 Go</span></td><td class='meter'><span class='bar '><i style='width:2.391304347826087%'></i></span><span class='v'>11 / 460 Go</span></td><td class='r mono'>1.08</td><td class='mono'>26.5.1</td></tr><tr><td class='id'>M2M32b</td><td>M2 Max</td><td class='r'>12</td><td class='r'>32 Go</td><td><span class='st on'>en ligne</span></td><td class='meter'><span class='bar '><i style='width:9.9%'></i></span><span class='v'>10 %</span></td><td class='meter'><span class='bar '><i style='width:23.125%'></i></span><span class='v'>7,4 / 32 Go</span></td><td class='meter'><span class='bar '><i style='width:2.391304347826087%'></i></span><span class='v'>11 / 460 Go</span></td><td class='r mono'>1.2</td><td class='mono'>26.5.1</td></tr><tr><td class='id'>M2M32c</td><td>M2 Max</td><td class='r'>12</td><td class='r'>32 Go</td><td><span class='st on'>en ligne</span></td><td class='meter'><span class='bar '><i style='width:12.3%'></i></span><span class='v'>12 %</span></td><td class='meter'><span class='bar '><i style='width:24.375%'></i></span><span class='v'>7,8 / 32 Go</span></td><td class='meter'><span class='bar '><i style='width:2.391304347826087%'></i></span><span class='v'>11 / 460 Go</span></td><td class='r mono'>1.3</td><td class='mono'>26.5.1</td></tr><tr><td class='id'>m4mc</td><td>M4 Pro</td><td class='r'>12</td><td class='r'>24 Go</td><td><span class='st on'>en ligne</span></td><td class='meter'><span class='bar '><i style='width:10.4%'></i></span><span class='v'>10 %</span></td><td class='meter'><span class='bar '><i style='width:31.25%'></i></span><span class='v'>7,5 / 24 Go</span></td><td class='meter'><span class='bar '><i style='width:2.391304347826087%'></i></span><span class='v'>11 / 460 Go</span></td><td class='r mono'>1.51</td><td class='mono'>26.5.1</td></tr><tr><td class='id'>M1M32</td><td>M1 Max</td><td class='r'>10</td><td class='r'>32 Go</td><td><span class='st on'>en ligne</span></td><td class='meter'><span class='bar '><i style='width:13.4%'></i></span><span class='v'>13 %</span></td><td class='meter'><span class='bar '><i style='width:25.0%'></i></span><span class='v'>8,0 / 32 Go</span></td><td class='meter'><span class='bar '><i style='width:2.391304347826087%'></i></span><span class='v'>11 / 460 Go</span></td><td class='r mono'>1.63</td><td class='mono'>26.5.1</td></tr><tr><td class='id'>m4ma</td><td>M4</td><td class='r'>10</td><td class='r'>24 Go</td><td><span class='st on'>en ligne</span></td><td class='meter'><span class='bar '><i style='width:12.5%'></i></span><span class='v'>12 %</span></td><td class='meter'><span class='bar '><i style='width:30.416666666666664%'></i></span><span class='v'>7,3 / 24 Go</span></td><td class='meter'><span class='bar '><i style='width:2.391304347826087%'></i></span><span class='v'>11 / 460 Go</span></td><td class='r mono'>1.45</td><td class='mono'>26.5.1</td></tr><tr><td class='id'>m4mb</td><td>M4</td><td class='r'>10</td><td class='r'>16 Go</td><td><span class='st on'>en ligne</span></td><td class='meter'><span class='bar '><i style='width:13.3%'></i></span><span class='v'>13 %</span></td><td class='meter'><span class='bar '><i style='width:46.25%'></i></span><span class='v'>7,4 / 16 Go</span></td><td class='meter'><span class='bar '><i style='width:4.824561403508771%'></i></span><span class='v'>11 / 228 Go</span></td><td class='r mono'>2.14</td><td class='mono'>26.5.1</td></tr><tr><td class='id'>m4md</td><td>M4</td><td class='r'>10</td><td class='r'>16 Go</td><td><span class='st off'>hors ligne</span></td><td class='meter'><span class='bar '><i style='width:0%'></i></span><span class='v'>—</span></td><td class='meter'><span class='bar '><i style='width:0%'></i></span><span class='v'>—</span></td><td class='meter'><span class='bar '><i style='width:0%'></i></span><span class='v'>—</span></td><td class='r mono'>—</td><td class='mono'>—</td></tr><tr><td class='id'>m2m16</td><td>M2 Pro</td><td class='r'>10</td><td class='r'>16 Go</td><td><span class='st on'>en ligne</span></td><td class='meter'><span class='bar '><i style='width:20.5%'></i></span><span class='v'>20 %</span></td><td class='meter'><span class='bar '><i style='width:46.875%'></i></span><span class='v'>7,5 / 16 Go</span></td><td class='meter'><span class='bar '><i style='width:2.391304347826087%'></i></span><span class='v'>11 / 460 Go</span></td><td class='r mono'>1.22</td><td class='mono'>26.4.1</td></tr><tr><td class='id'>M1M64</td><td>M1 Max</td><td class='r'>10</td><td class='r'>64 Go</td><td><span class='st on'>en ligne</span></td><td class='meter'><span class='bar '><i style='width:12.2%'></i></span><span class='v'>12 %</span></td><td class='meter'><span class='bar '><i style='width:12.968750000000002%'></i></span><span class='v'>8,3 / 64 Go</span></td><td class='meter'><span class='bar '><i style='width:0.5920344456404736%'></i></span><span class='v'>11 / 1 858 Go</span></td><td class='r mono'>1.54</td><td class='mono'>26.6.2</td></tr><tr><td class='id'>m2m16b</td><td>M2 Pro</td><td class='r'>10</td><td class='r'>16 Go</td><td><span class='st on'>en ligne</span></td><td class='meter'><span class='bar '><i style='width:25.9%'></i></span><span class='v'>26 %</span></td><td class='meter'><span class='bar '><i style='width:51.87500000000001%'></i></span><span class='v'>8,3 / 16 Go</span></td><td class='meter'><span class='bar '><i style='width:1.6198704103671708%'></i></span><span class='v'>15 / 926 Go</span></td><td class='r mono'>2.36</td><td class='mono'>26.6</td></tr><tr><td class='id'>m4me</td><td>M4</td><td class='r'>10</td><td class='r'>16 Go</td><td><span class='st on'>en ligne</span></td><td class='meter'><span class='bar '><i style='width:14%'></i></span><span class='v'>14 %</span></td><td class='meter'><span class='bar '><i style='width:55.625%'></i></span><span class='v'>8,9 / 16 Go</span></td><td class='meter'><span class='bar '><i style='width:4.824561403508771%'></i></span><span class='v'>11 / 228 Go</span></td><td class='r mono'>1.5</td><td class='mono'>26.6.2</td></tr><tr><td class='id'>m4mf</td><td>M4</td><td class='r'>10</td><td class='r'>16 Go</td><td><span class='st on'>en ligne</span></td><td class='meter'><span class='bar '><i style='width:11.5%'></i></span><span class='v'>12 %</span></td><td class='meter'><span class='bar '><i style='width:54.37499999999999%'></i></span><span class='v'>8,7 / 16 Go</span></td><td class='meter'><span class='bar '><i style='width:4.824561403508771%'></i></span><span class='v'>11 / 228 Go</span></td><td class='r mono'>1.66</td><td class='mono'>26.6.2</td></tr><tr><td class='id'>m4mg</td><td>M4</td><td class='r'>10</td><td class='r'>16 Go</td><td><span class='st on'>en ligne</span></td><td class='meter'><span class='bar '><i style='width:10%'></i></span><span class='v'>10 %</span></td><td class='meter'><span class='bar '><i style='width:53.75%'></i></span><span class='v'>8,6 / 16 Go</span></td><td class='meter'><span class='bar '><i style='width:4.824561403508771%'></i></span><span class='v'>11 / 228 Go</span></td><td class='r mono'>1.41</td><td class='mono'>26.6.2</td></tr><tr><td class='id'>m4mh</td><td>M4</td><td class='r'>10</td><td class='r'>16 Go</td><td><span class='st on'>en ligne</span></td><td class='meter'><span class='bar '><i style='width:12.6%'></i></span><span class='v'>13 %</span></td><td class='meter'><span class='bar '><i style='width:56.875%'></i></span><span class='v'>9,1 / 16 Go</span></td><td class='meter'><span class='bar '><i style='width:4.824561403508771%'></i></span><span class='v'>11 / 228 Go</span></td><td class='r mono'>1.63</td><td class='mono'>26.6.2</td></tr><tr><td class='id'>m4mi</td><td>M4</td><td class='r'>10</td><td class='r'>16 Go</td><td><span class='st on'>en ligne</span></td><td class='meter'><span class='bar '><i style='width:14.7%'></i></span><span class='v'>15 %</span></td><td class='meter'><span class='bar '><i style='width:55.00000000000001%'></i></span><span class='v'>8,8 / 16 Go</span></td><td class='meter'><span class='bar '><i style='width:4.824561403508771%'></i></span><span class='v'>11 / 228 Go</span></td><td class='r mono'>1.57</td><td class='mono'>26.6.2</td></tr><tr><td class='id'>m4mj</td><td>M4</td><td class='r'>10</td><td class='r'>16 Go</td><td><span class='st on'>en ligne</span></td><td class='meter'><span class='bar '><i style='width:11.8%'></i></span><span class='v'>12 %</span></td><td class='meter'><span class='bar '><i style='width:55.00000000000001%'></i></span><span class='v'>8,8 / 16 Go</span></td><td class='meter'><span class='bar '><i style='width:4.824561403508771%'></i></span><span class='v'>11 / 228 Go</span></td><td class='r mono'>1.17</td><td class='mono'>26.6.2</td></tr><tr><td class='id'>m4mk</td><td>M4</td><td class='r'>10</td><td class='r'>16 Go</td><td><span class='st on'>en ligne</span></td><td class='meter'><span class='bar '><i style='width:11%'></i></span><span class='v'>11 %</span></td><td class='meter'><span class='bar '><i style='width:53.75%'></i></span><span class='v'>8,6 / 16 Go</span></td><td class='meter'><span class='bar '><i style='width:4.824561403508771%'></i></span><span class='v'>11 / 228 Go</span></td><td class='r mono'>1.42</td><td class='mono'>26.6.2</td></tr><tr><td class='id'>M3BA24</td><td>M3</td><td class='r'>8</td><td class='r'>24 Go</td><td><span class='st on'>en ligne</span></td><td class='meter'><span class='bar '><i style='width:14.5%'></i></span><span class='v'>14 %</span></td><td class='meter'><span class='bar '><i style='width:43.333333333333336%'></i></span><span class='v'>10,4 / 24 Go</span></td><td class='meter'><span class='bar '><i style='width:2.391304347826087%'></i></span><span class='v'>11 / 460 Go</span></td><td class='r mono'>1.89</td><td class='mono'>26.5.1</td></tr><tr><td class='id'>M3BA16</td><td>M3</td><td class='r'>8</td><td class='r'>16 Go</td><td><span class='st on'>en ligne</span></td><td class='meter'><span class='bar '><i style='width:17%'></i></span><span class='v'>17 %</span></td><td class='meter'><span class='bar '><i style='width:58.12500000000001%'></i></span><span class='v'>9,3 / 16 Go</span></td><td class='meter'><span class='bar '><i style='width:2.608695652173913%'></i></span><span class='v'>12 / 460 Go</span></td><td class='r mono'>1.36</td><td class='mono'>26.5.1</td></tr><tr><td class='id'>m2m8a</td><td>M2</td><td class='r'>8</td><td class='r'>8 Go</td><td><span class='st on'>en ligne</span></td><td class='meter'><span class='bar '><i style='width:18.5%'></i></span><span class='v'>18 %</span></td><td class='meter'><span class='bar '><i style='width:58.75%'></i></span><span class='v'>4,7 / 8 Go</span></td><td class='meter'><span class='bar '><i style='width:2.391304347826087%'></i></span><span class='v'>11 / 460 Go</span></td><td class='r mono'>1.4</td><td class='mono'>26.5.1</td></tr><tr><td class='id'>m2m8b</td><td>M2</td><td class='r'>8</td><td class='r'>8 Go</td><td><span class='st on'>en ligne</span></td><td class='meter'><span class='bar '><i style='width:12.5%'></i></span><span class='v'>12 %</span></td><td class='meter'><span class='bar '><i style='width:58.75%'></i></span><span class='v'>4,7 / 8 Go</span></td><td class='meter'><span class='bar '><i style='width:4.824561403508771%'></i></span><span class='v'>11 / 228 Go</span></td><td class='r mono'>1.3</td><td class='mono'>26.5.1</td></tr><tr><td class='id'>m1m16</td><td>M1</td><td class='r'>8</td><td class='r'>16 Go</td><td><span class='st on'>en ligne</span></td><td class='meter'><span class='bar '><i style='width:12%'></i></span><span class='v'>12 %</span></td><td class='meter'><span class='bar '><i style='width:53.75%'></i></span><span class='v'>8,6 / 16 Go</span></td><td class='meter'><span class='bar '><i style='width:1.187904967602592%'></i></span><span class='v'>11 / 926 Go</span></td><td class='r mono'>1.07</td><td class='mono'>26.6.2</td></tr><tr><td class='id'>m2m16c</td><td>M2</td><td class='r'>8</td><td class='r'>16 Go</td><td><span class='st on'>en ligne</span></td><td class='meter'><span class='bar '><i style='width:13.3%'></i></span><span class='v'>13 %</span></td><td class='meter'><span class='bar '><i style='width:53.75%'></i></span><span class='v'>8,6 / 16 Go</span></td><td class='meter'><span class='bar '><i style='width:1.187904967602592%'></i></span><span class='v'>11 / 926 Go</span></td><td class='r mono'>1.54</td><td class='mono'>26.6.2</td></tr><tr><td class='id'>m1m16b</td><td>M1</td><td class='r'>8</td><td class='r'>16 Go</td><td><span class='st on'>en ligne</span></td><td class='meter'><span class='bar '><i style='width:16%'></i></span><span class='v'>16 %</span></td><td class='meter'><span class='bar '><i style='width:56.25%'></i></span><span class='v'>9,0 / 16 Go</span></td><td class='meter'><span class='bar '><i style='width:6.578947368421052%'></i></span><span class='v'>15 / 228 Go</span></td><td class='r mono'>1.27</td><td class='mono'>26.6.1</td></tr></tbody></table> | |
| 222 | +</section> | |
| 223 | + | |
| 224 | +<section class='sec'><div class='sec-h'><span class='num'>10</span><h2>Sources et méthode</h2></div> | |
| 225 | +<p><code>mld scan</code>, <code>mld status</code>, <code>mld tunnel status</code> (22 h 30), <code>system_profiler SPHardwareDataType</code>, <code>sw_vers</code>, <code>sysctl hw.model</code>, <code>df -H</code>, <code>uptime</code>, <code>pm2 jlist</code> sur les 33 Macs ; <code>lscpu</code>, <code>free</code>, <code>/proc/mdstat</code>, <code>systemctl</code>, <code>ufw status</code> sur les 4 serveurs OVH ; fiches du manager OVH (ID, baie, IPv6) ; <code>NODES.md</code>, <code>~/.ssh/config</code>, <code>mld/config.py</code>, <code>agentd.py</code>. Géolocalisation des Macs loués : whois / ip-api et temps de connexion mesurés le 9 septembre. Documents complémentaires : <code>NODES.md</code> (identifiants, sudo), <code>MACLUSTR-TUNNEL.md</code> (tunnel), <code>KA-REMOTE-DEV.md</code> (apps Ka), <code>ovh-manager/README.md</code> (installation OVH), <code>cluster-registry.json</code> (registre mld).</p> | |
| 226 | +<p class='note'>Les identifiants (mots de passe sudo, DeskIn, VNC, jetons) ne figurent pas dans ce dossier : voir <code>NODES.md</code>. Document généré par <code>cluster-skill/dossier/build.py</code> à partir de <code>INVENTAIRE-MACLUSTR.md</code>, de l'inventaire du site www.maclustr.io et de l'API live du cluster ; à régénérer après tout ajout ou retrait de machine.</p> | |
| 227 | +<div class='end'><svg class='logo' viewBox='0 0 32 32'><rect x='3' y='3' width='11' height='11' rx='2.5' fill='currentColor'/><rect x='18' y='3' width='11' height='11' rx='2.5' fill='currentColor' opacity='.55'/><rect x='3' y='18' width='11' height='11' rx='2.5' fill='currentColor' opacity='.55'/><rect x='18' y='18' width='11' height='11' rx='2.5' fill='currentColor' opacity='.3'/></svg><span>MacLustr · 10 septembre 2026</span></div> | |
| 228 | +</section> | |
| 229 | +</body></html> | |
| \ No newline at end of file | ||
added
dossier/build.py
+284 −0
@@ -0,0 +1,284 @@ | ||
| 1 | +#!/usr/bin/env python3 | |
| 2 | +"""Dossier technique MacLustr — génère dossier.html (couverture + corps) à partir de : | |
| 3 | + - INVENTAIRE-MACLUSTR.md (tables mesurées : matériel, rôles, loués, OVH, tunnel, répartitions) | |
| 4 | + - data.json (inventaire du site www.maclustr.io : nœuds, sites géolocalisés, apps) | |
| 5 | + - live.json (instantané /api/live) | |
| 6 | + Rendu PDF : print.mjs (Playwright/Chromium) puis fusion couverture + corps (PyMuPDF).""" | |
| 7 | +import json, re, html, datetime, pathlib | |
| 8 | + | |
| 9 | +HERE = pathlib.Path(__file__).parent | |
| 10 | +MD = (HERE.parent / "INVENTAIRE-MACLUSTR.md").read_text() | |
| 11 | +D = json.loads((HERE / "data.json").read_text()) | |
| 12 | +L = json.loads((HERE / "live.json").read_text()) | |
| 13 | +NOW = datetime.datetime.now() | |
| 14 | +DATE = NOW.strftime("%-d %B %Y").replace("September", "septembre").replace("October", "octobre") | |
| 15 | +MONTHS = {"January":"janvier","February":"février","March":"mars","April":"avril","May":"mai","June":"juin","July":"juillet","August":"août","September":"septembre","October":"octobre","November":"novembre","December":"décembre"} | |
| 16 | +for en, fr in MONTHS.items(): DATE = DATE.replace(en, fr) | |
| 17 | +TIME = NOW.strftime("%H h %M") | |
| 18 | + | |
| 19 | +# ---------- markdown helpers ---------- | |
| 20 | +def inline(s): | |
| 21 | + s = html.escape(s, quote=False) | |
| 22 | + s = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", s) | |
| 23 | + s = re.sub(r"`([^`]+)`", r"<code>\1</code>", s) | |
| 24 | + s = s.replace("—", "—") | |
| 25 | + return s | |
| 26 | + | |
| 27 | +def table_after(heading_regex): | |
| 28 | + """Renvoie (header_cells, rows) de la première table après le titre correspondant.""" | |
| 29 | + m = re.search(heading_regex, MD, re.M) | |
| 30 | + if not m: raise SystemExit(f"titre introuvable : {heading_regex}") | |
| 31 | + rest = MD[m.end():] | |
| 32 | + lines = rest.splitlines() | |
| 33 | + i = 0 | |
| 34 | + while i < len(lines) and not lines[i].startswith("|"): i += 1 | |
| 35 | + tbl = [] | |
| 36 | + while i < len(lines) and lines[i].startswith("|"): | |
| 37 | + tbl.append(lines[i]); i += 1 | |
| 38 | + cells = lambda ln: [c.strip() for c in ln.strip().strip("|").split("|")] | |
| 39 | + head = cells(tbl[0]); rows = [cells(r) for r in tbl[2:]] | |
| 40 | + return head, rows | |
| 41 | + | |
| 42 | +def para_after(heading_regex, n=1): | |
| 43 | + m = re.search(heading_regex, MD, re.M); rest = MD[m.end():].strip().splitlines() | |
| 44 | + out = [] | |
| 45 | + for ln in rest: | |
| 46 | + if ln.startswith("|") or ln.startswith("#"): break | |
| 47 | + if ln.strip(): out.append(inline(ln.strip())) | |
| 48 | + if len(out) >= n: break | |
| 49 | + return out | |
| 50 | + | |
| 51 | +def html_table(head, rows, cls="", align_right=(), nowrap=(), widths=None): | |
| 52 | + th = "".join(f"<th{' class=r' if i in align_right else ''}>{inline(h)}</th>" for i, h in enumerate(head)) | |
| 53 | + trs = [] | |
| 54 | + for r in rows: | |
| 55 | + tds = [] | |
| 56 | + for i, c in enumerate(r): | |
| 57 | + classes = [] | |
| 58 | + if i in align_right: classes.append("r") | |
| 59 | + if i in nowrap: classes.append("nw") | |
| 60 | + if i == 0: classes.append("id") | |
| 61 | + attr = (' class="' + ' '.join(classes) + '"') if classes else '' | |
| 62 | + tds.append(f"<td{attr}>{inline(c)}</td>") | |
| 63 | + trs.append("<tr>" + "".join(tds) + "</tr>") | |
| 64 | + colg = "".join(f"<col style='width:{w}'>" for w in widths) if widths else "" | |
| 65 | + return f"<table class='t {cls}'>{colg}<thead><tr>{th}</tr></thead><tbody>{''.join(trs)}</tbody></table>" | |
| 66 | + | |
| 67 | +# ---------- data ---------- | |
| 68 | +nodes = D["nodes"]; sites = D["sites"]; apps = D["apps"]; groups = D["appGroups"] | |
| 69 | +by_site = {} | |
| 70 | +for n in nodes: by_site.setdefault(n["site"], []).append(n) | |
| 71 | +apps_by_node = {} | |
| 72 | +for a in apps: apps_by_node.setdefault(a["node"], []).append(a) | |
| 73 | +macs = [n for n in nodes if n["group"] != "ovh"] | |
| 74 | +tot = dict(nodes=len(nodes), macs=len(macs), ovh=len(nodes) - len(macs), cores=sum(n["cores"] for n in nodes), | |
| 75 | + gpu=sum(n.get("gpu") or 0 for n in macs), ram=sum(n["ram"] for n in nodes), apps=len(apps), | |
| 76 | + web=sum(1 for a in apps if a.get("domain")), sites=len(sites), | |
| 77 | + countries=len({s["region"].split(",")[-1].strip() for s in sites.values()})) | |
| 78 | +S = L.get("summary") or {} | |
| 79 | +lnodes = L.get("nodes", {}); lapps = L.get("apps", {}); ltun = L.get("tunnel", []) | |
| 80 | +fr = lambda n: f"{n:,}".replace(",", " ") | |
| 81 | +def ram_fmt(gb): return f"{gb/1024:.2f}".replace(".", ",") + " To" if gb >= 1000 else f"{gb} Go" | |
| 82 | + | |
| 83 | +# ---------- pieces ---------- | |
| 84 | +def kpi(v, l, s=""): return f"<div class='kpi'><b>{v}</b><span>{l}</span>{f'<small>{s}</small>' if s else ''}</div>" | |
| 85 | + | |
| 86 | +def bar(p, cls=""): | |
| 87 | + p = 0 if p is None else max(0, min(100, p)) | |
| 88 | + return f"<span class='bar {cls}'><i style='width:{p}%'></i></span>" | |
| 89 | + | |
| 90 | +def status_cell(n): | |
| 91 | + if n["group"] == "ovh": | |
| 92 | + g = next((g for g in ltun if g["name"] == n["id"]), None) | |
| 93 | + if g: return "<span class='st on'>passerelle active</span>" if g["ok"] else "<span class='st bad'>injoignable</span>" | |
| 94 | + return "<span class='st on'>en réserve</span>" | |
| 95 | + l = lnodes.get(n["id"]) | |
| 96 | + if not l or not l.get("online"): return "<span class='st off'>hors ligne</span>" | |
| 97 | + return "<span class='st on'>en ligne</span>" | |
| 98 | + | |
| 99 | +# heatmap live | |
| 100 | +def heat_rows(): | |
| 101 | + out = [] | |
| 102 | + for n in sorted(macs, key=lambda x: -(x["cores"])): | |
| 103 | + l = lnodes.get(n["id"]) or {} | |
| 104 | + on = l.get("online") | |
| 105 | + cpu = l.get("cpu"); mem = l.get("memUsedGB"); memT = l.get("memTotalGB") or n["ram"] | |
| 106 | + disk = l.get("diskUsedGB"); diskT = l.get("diskTotalGB") | |
| 107 | + out.append(f"<tr><td class='id'>{n['id']}</td><td>{n['chip']}</td><td class='r'>{n['cores']}</td><td class='r'>{n['ram']} Go</td>" | |
| 108 | + f"<td>{status_cell(n)}</td>" | |
| 109 | + f"<td class='meter'>{bar(cpu if on else None)}<span class='v'>{(str(round(cpu)) + ' %') if on and cpu is not None else '—'}</span></td>" | |
| 110 | + f"<td class='meter'>{bar((mem / memT * 100) if on and mem is not None and memT else None)}<span class='v'>{(f'{mem:.1f}'.replace('.', ',') + ' / ' + str(memT) + ' Go') if on and mem is not None else '—'}</span></td>" | |
| 111 | + f"<td class='meter'>{bar((disk / diskT * 100) if on and disk and diskT else None)}<span class='v'>{(fr(int(disk)) + ' / ' + fr(int(diskT)) + ' Go') if on and disk and diskT else '—'}</span></td>" | |
| 112 | + f"<td class='r mono'>{l.get('load1', '—') if on else '—'}</td><td class='mono'>{l.get('os', '—') if on else '—'}</td></tr>") | |
| 113 | + return "".join(out) | |
| 114 | + | |
| 115 | +# répartition par site (barres) | |
| 116 | +def site_bars(): | |
| 117 | + rows = [] | |
| 118 | + mx_c = max(sum(n["cores"] for n in ns) for ns in by_site.values()) | |
| 119 | + mx_r = max(sum(n["ram"] for n in ns) for ns in by_site.values()) | |
| 120 | + for k, s in sorted(sites.items(), key=lambda kv: -len(by_site.get(kv[0], []))): | |
| 121 | + ns = by_site.get(k, []); c = sum(n["cores"] for n in ns); r = sum(n["ram"] for n in ns) | |
| 122 | + rows.append(f"<tr><td class='id'>{s['name']}<br><small>{s['region']} · {s['operator']}</small></td><td class='r'>{len(ns)}</td>" | |
| 123 | + f"<td class='meter wide'>{bar(c / mx_c * 100)}<span class='v'>{c} {'thr' if s['kind'] == 'ovh' else 'cœurs'}</span></td>" | |
| 124 | + f"<td class='meter wide'>{bar(r / mx_r * 100, 'alt')}<span class='v'>{ram_fmt(r)}</span></td>" | |
| 125 | + f"<td class='mono small'>{', '.join(n['id'] for n in ns)}</td></tr>") | |
| 126 | + return "".join(rows) | |
| 127 | + | |
| 128 | +def apps_section(): | |
| 129 | + out = [] | |
| 130 | + for g in groups: | |
| 131 | + ga = [a for a in apps if a["group"] == g["id"]] | |
| 132 | + rows = [] | |
| 133 | + for a in ga: | |
| 134 | + l = lapps.get(a["id"]) or {} | |
| 135 | + st = "<span class='st on'>en ligne</span>" if l.get("state") == "up" else ("<span class='st bad'>" + html.escape(l.get("state", "—")) + "</span>" if l else "<span class='st off'>—</span>") | |
| 136 | + extra = [] | |
| 137 | + if l.get("publicMs") is not None and l.get("publicOk"): extra.append(f"{l['publicMs']} ms") | |
| 138 | + if l.get("uptime24h") is not None: extra.append(f"{round(l['uptime24h'])} % / 24 h") | |
| 139 | + rows.append(f"<tr><td class='id'>{html.escape(a['name'])}</td><td class='mono'>{html.escape(a['domain'] or 'service interne')}</td><td class='mono nw'>{l.get('node') or a['node']}<span class='faint'>:{a['port']}</span></td><td>{html.escape(a['desc'])}</td><td class='nw'>{st} <span class='faint mono small'>{' · '.join(extra)}</span></td></tr>") | |
| 140 | + out.append(f"<div class='app-group{' keep' if len(ga) <= 6 else ''}'><h3>{html.escape(g['name'])} <span class='cnt'>{len(ga)}</span></h3><p class='lead'>{html.escape(g['blurb'])}</p>" | |
| 141 | + f"<table class='t apps'><col style='width:15%'><col style='width:19%'><col style='width:11%'><col><col style='width:15%'><thead><tr><th>Application</th><th>Domaine</th><th>Nœud</th><th>Description</th><th>État</th></tr></thead><tbody>{''.join(rows)}</tbody></table></div>") | |
| 142 | + return "".join(out) | |
| 143 | + | |
| 144 | +# ---------- tables issues du markdown ---------- | |
| 145 | +h_over, r_over = table_after(r"^## Vue d'ensemble") | |
| 146 | +h_1a, r_1a = table_after(r"^### 1a\.") | |
| 147 | +h_1b, r_1b = table_after(r"^### 1b\.") | |
| 148 | +h_2a, r_2a = table_after(r"^### 2a\.") | |
| 149 | +h_2b, r_2b = table_after(r"^### 2b\.") | |
| 150 | +h_2c, r_2c = table_after(r"^### 2c\.") | |
| 151 | +h_3a, r_3a = table_after(r"^### 3a\.") | |
| 152 | +h_4, r_4 = table_after(r"^## 4\.") | |
| 153 | +h_5a, r_5a = table_after(r"^### 5a\.") | |
| 154 | +h_5b, r_5b = table_after(r"^### 5b\.") | |
| 155 | +intro_over = para_after(r"^## Vue d'ensemble\n\n\|.*\n\|.*\n(\|.*\n)+", 3) | |
| 156 | +intro_1 = para_after(r"^## 1\. Nœuds internes.*$") | |
| 157 | +note_1a = para_after(r"^\*\*Sous-total LAN") | |
| 158 | +intro_1b = para_after(r"^### 1b\.") | |
| 159 | +intro_2 = para_after(r"^## 2\. Macs loués.*$") | |
| 160 | +intro_2a = para_after(r"^### 2a\.") | |
| 161 | +intro_2b = para_after(r"^### 2b\.") | |
| 162 | +intro_2c = para_after(r"^### 2c\.") | |
| 163 | +intro_3 = para_after(r"^## 3\. Serveurs.*$") | |
| 164 | +fiches_3b = [inline(l.strip()[2:]) for l in MD.split("### 3b. Fiches")[1].split("## 4.")[0].strip().splitlines() if l.strip().startswith("- ")] | |
| 165 | +intro_4 = para_after(r"^## 4\. Topologie.*$") | |
| 166 | +note_4 = [inline(l.strip()) for l in MD.split("## 4. Topologie publique : sites et tunnel")[1].split("## 5.")[0].strip().split("\n\n") if l.strip().startswith("Autres services")] | |
| 167 | +cap_5c = para_after(r"^### 5c\.") | |
| 168 | +sources = para_after(r"^## 6\. Sources.*$") | |
| 169 | + | |
| 170 | +# lignes des tables 3a et 1a : retirer la colonne N° de série ? on garde tout (document détaillé) | |
| 171 | +# ---------- HTML ---------- | |
| 172 | +CSS = (HERE / "dossier.css").read_text() | |
| 173 | +LOGO = "<svg class='logo' viewBox='0 0 32 32'><rect x='3' y='3' width='11' height='11' rx='2.5' fill='currentColor'/><rect x='18' y='3' width='11' height='11' rx='2.5' fill='currentColor' opacity='.55'/><rect x='3' y='18' width='11' height='11' rx='2.5' fill='currentColor' opacity='.55'/><rect x='18' y='18' width='11' height='11' rx='2.5' fill='currentColor' opacity='.3'/></svg>" | |
| 174 | + | |
| 175 | +cover = f"""<!doctype html><html lang='fr'><head><meta charset='utf-8'><title>MacLustr — Dossier technique</title><style>{CSS} @page {{ size: 297mm 210mm; margin: 0; }}</style></head> | |
| 176 | +<body class='cover'> | |
| 177 | +<div class='cv'> | |
| 178 | + <div class='cv-top'><div class='brand'>{LOGO}<span>MacLustr</span></div><span class='mono'>www.maclustr.io</span></div> | |
| 179 | + <div class='cv-mid'> | |
| 180 | + <p class='eyebrow'>Dossier technique · inventaire complet</p> | |
| 181 | + <h1>Un supercalculateur<br><span>fait de Macs.</span></h1> | |
| 182 | + <p class='sub'>{tot['macs']} Macs Apple Silicon et {tot['ovh']} serveurs OVHcloud, répartis dans {tot['sites']} sites et {tot['countries']} pays, orchestrés comme une seule plateforme. Nœuds internes, Macs loués, passerelles, tunnel, applications hébergées : tout ce qui compose le cluster au {DATE}.</p> | |
| 183 | + </div> | |
| 184 | + <div class='cv-facts'> | |
| 185 | + {kpi(tot['nodes'], 'nœuds', f"{tot['macs']} Macs + {tot['ovh']} Linux")}{kpi(fr(tot['cores']), 'cœurs CPU', f"{S.get('coresOnline', '—')} actifs")}{kpi(fr(tot['gpu']), 'cœurs GPU', 'Apple Silicon')}{kpi(ram_fmt(tot['ram']), 'mémoire', f"{S.get('ramUsedGB', '—')} Go utilisés")}{kpi(tot['apps'], 'applications', f"{(S.get('apps') or {}).get('up', '—')} saines")}{kpi(tot['web'], 'sites HTTPS', 'MacLustr Tunnel')} | |
| 186 | + </div> | |
| 187 | + <div class='cv-bot'><span>Simon-Pierre Boucher · Saint-Augustin-de-Desmaures, Québec</span><span class='mono'>contact@spboucher.ai · état live du {DATE} à {TIME}</span></div> | |
| 188 | +</div> | |
| 189 | +</body></html>""" | |
| 190 | + | |
| 191 | +def sec(num, title, lead=""): | |
| 192 | + return f"<section class='sec'><div class='sec-h'><span class='num'>{num}</span><h2>{title}</h2></div>{f'<p class=lead>{lead}</p>' if lead else ''}" | |
| 193 | + | |
| 194 | +toc = [("1", "Vue d'ensemble"), ("2", "Nœuds internes — LAN Saint-Augustin"), ("3", "Macs dédiés loués hors site"), ("4", "Serveurs dédiés OVHcloud"), | |
| 195 | + ("5", "Topologie publique : tunnel et sites"), ("6", "Emplacements et carte"), ("7", "Répartitions et capacité"), ("8", "Applications hébergées"), ("9", "Instantané live"), ("10", "Sources et méthode")] | |
| 196 | + | |
| 197 | +body = f"""<!doctype html><html lang='fr'><head><meta charset='utf-8'><title>MacLustr — Dossier technique</title><style>{CSS}</style></head> | |
| 198 | +<body> | |
| 199 | +{sec('1', "Vue d'ensemble")} | |
| 200 | +<div class='two'> | |
| 201 | + <div> | |
| 202 | + <p class='lead big'>MacLustr est un cluster privé de {tot['macs']} Macs Apple Silicon et {tot['ovh']} serveurs Linux OVHcloud. Il totalise <strong>{fr(tot['cores'])} cœurs CPU</strong>, <strong>{fr(tot['gpu'])} cœurs GPU</strong> et <strong>{ram_fmt(tot['ram'])} de mémoire</strong>, et héberge <strong>{tot['apps']} applications</strong> en production, dont {tot['web']} sites publics servis en HTTPS par le MacLustr Tunnel.</p> | |
| 203 | + {''.join(f'<p>{p}</p>' for p in intro_over)} | |
| 204 | + </div> | |
| 205 | + <div> | |
| 206 | + <div class='toc'><h4>Sommaire</h4>{''.join(f"<div><span class='num'>{n}</span>{t}</div>" for n, t in toc)}</div> | |
| 207 | + </div> | |
| 208 | +</div> | |
| 209 | +<h3>Par catégorie</h3> | |
| 210 | +{html_table(h_over, r_over, 'over', align_right=(1, 2, 3), nowrap=(1, 2, 3))} | |
| 211 | +<div class='kpis'>{kpi(len(by_site['staug']), 'Macs sur le LAN', 'Saint-Augustin')}{kpi(sum(len(by_site.get(k, [])) for k in ('ist', 'atl', 'dub', 'lim')), 'Macs loués', 'Istanbul · Atlanta · Dublin · Limassol')}{kpi(len(by_site['bhs']) + len(by_site['gra']), 'serveurs OVHcloud', 'Beauharnois · Gravelines')}{kpi(len([n for n in nodes if 'gateway' in n['tags']]), 'passerelles', 'M1M32 · BHS64 · R9128')}{kpi(len(ltun), 'hubs WireGuard', f"{sum(len(g['peers']) for g in ltun)} pairs")}{kpi(sum(g.get('routes') or 0 for g in ltun), 'routes HTTPS', "Caddy · Let’s Encrypt")}</div> | |
| 212 | +</section> | |
| 213 | + | |
| 214 | +{sec('2', 'Nœuds internes — LAN Saint-Augustin-de-Desmaures', intro_1[0] if intro_1 else '')} | |
| 215 | +<h3>2a. Matériel et système <span class='cnt'>{len(r_1a)} Macs</span></h3> | |
| 216 | +{html_table(h_1a, r_1a, 'hw', align_right=(3, 4), nowrap=(0, 3, 4, 5, 7, 8, 9))} | |
| 217 | +<p class='note'>{note_1a[0] if note_1a else ''}</p> | |
| 218 | +<div class='roles-page'><h3>2b. Rôle, runtimes et applications par nœud</h3> | |
| 219 | +<p class='lead'>{intro_1b[0] if intro_1b else ''}</p> | |
| 220 | +{html_table(h_1b, r_1b, 'roles', widths=('7%', '21%', '18%', '26%', '28%'))}</div> | |
| 221 | +</section> | |
| 222 | + | |
| 223 | +{sec('3', 'Macs dédiés loués hors site', intro_2[0] if intro_2 else '')} | |
| 224 | +<h3>3a. Turquie — Macly <span class='cnt'>{len(r_2a)} Macs</span></h3> | |
| 225 | +<p class='lead'>{intro_2a[0] if intro_2a else ''}</p> | |
| 226 | +{html_table(h_2a, r_2a, 'hw', align_right=(3, 4), nowrap=(0, 3, 4, 5, 8, 9, 10, 11, 12))} | |
| 227 | +<div class='keep'><h3>3b. États-Unis et Irlande — MacStadium <span class='cnt'>{len(r_2b)} Macs</span></h3> | |
| 228 | +<p class='lead'>{intro_2b[0] if intro_2b else ''}</p> | |
| 229 | +{html_table(h_2b, r_2b, 'hw', align_right=(3, 4), nowrap=(0, 3, 4, 5, 7, 8, 10, 11))}</div> | |
| 230 | +<div class='keep'><h3>3c. Chypre — rentamac <span class='cnt'>{len(r_2c)} Mac</span></h3> | |
| 231 | +<p class='lead'>{intro_2c[0] if intro_2c else ''}</p> | |
| 232 | +{html_table(h_2c, r_2c, 'hw', align_right=(3, 4), nowrap=(0, 3, 4, 5, 7, 8, 9, 10))}</div> | |
| 233 | +</section> | |
| 234 | + | |
| 235 | +{sec('4', 'Serveurs dédiés OVHcloud (Linux)', intro_3[0] if intro_3 else '')} | |
| 236 | +{html_table(h_3a, r_3a, 'ovh', align_right=(3,), nowrap=(3,))} | |
| 237 | +<h3>Fiches</h3> | |
| 238 | +<div class='fiches'>{''.join(f'<p>{f}</p>' for f in fiches_3b)}</div> | |
| 239 | +</section> | |
| 240 | + | |
| 241 | +{sec('5', 'Topologie publique : tunnel et sites', intro_4[0] if intro_4 else '')} | |
| 242 | +{html_table(h_4, r_4, 'tun', widths=('22%', '78%'))} | |
| 243 | +<div class='three-col'> | |
| 244 | + <div class='panel'><h4>Chemin d'une requête</h4> | |
| 245 | + <ol class='flow'><li><div><b>DNS</b><span>A → 51.161.112.61 (BHS64, Beauharnois) ; secours 51.255.75.61 (R9128, Gravelines)</span></div></li><li><div><b>Caddy</b><span>termine le TLS Let’s Encrypt et choisit l'upstream d'après l'hôte</span></div></li><li><div><b>WireGuard</b><span>hub 10.67.0.1 → interface wg1 du nœud (10.67.0.x), MTU 1380, keepalive 25 s</span></div></li><li><div><b>Nœud</b><span>processus PM2 ou launchd sur le port local ; healthcheck local puis public à chaque déploiement</span></div></li></ol> | |
| 246 | + </div> | |
| 247 | + <div class='panel'><h4>Passerelles live</h4> | |
| 248 | + {''.join(f"<div class='gw'><b>{g['name']}</b><span>{'principale' if g.get('primary') else 'secondaire'} · {g.get('site', '')}</span><span class='mono'>{g.get('routes') or 0} routes · {sum(1 for p in g['peers'] if p['online'])}/{len(g['peers'])} pairs · {str(round(sum(p['rx'] for p in g['peers']) / 1e9, 1)).replace('.', ',')} Go reçus</span></div>" for g in ltun)} | |
| 249 | + </div> | |
| 250 | + <div class='panel'><h4>Services internes</h4>{f"<p class='small' style='color:var(--ink-2)'>{note_4[0].replace('Autres services internes : ', '')}</p>" if note_4 else ''}</div> | |
| 251 | +</div> | |
| 252 | +</section> | |
| 253 | + | |
| 254 | +{sec('6', 'Emplacements et carte', f"{tot['sites']} sites dans {tot['countries']} pays sur trois continents. Le LAN de Saint-Augustin porte le cœur du cluster ; Beauharnois et Gravelines publient les sites ; Istanbul, Atlanta, Dublin et Limassol étendent la capacité. Géolocalisation vérifiée par IP et latence.")} | |
| 255 | +<figure class='map'><img src='map.png' alt='Carte mondiale des nœuds'><figcaption>Vue d'ensemble : épingles dimensionnées selon le nombre de nœuds, arcs WireGuard vers les passerelles OVHcloud. Capture du site www.maclustr.io/map.</figcaption></figure> | |
| 256 | +<div class='sites'>{''.join(f"<div class='site'><div class='s-h'><b>{s['name']}</b><span class='mono'>{len(by_site.get(k, []))} nœud{'s' if len(by_site.get(k, [])) > 1 else ''}</span></div><div class='s-r'>{s['region']} · {s['operator']} · <span class='mono'>{s['lat']:.2f}, {s['lon']:.2f}</span></div><p>{s['note']}</p><div class='s-n'>{''.join('<span>' + n['id'] + '</span>' for n in by_site.get(k, []))}</div></div>" for k, s in sorted(sites.items(), key=lambda kv: -len(by_site.get(kv[0], []))))}</div> | |
| 257 | +</section> | |
| 258 | + | |
| 259 | +{sec('7', 'Répartitions et capacité')} | |
| 260 | +<h3>7a. Par emplacement</h3> | |
| 261 | +<table class='t bars'><col style='width:24%'><col style='width:6%'><col style='width:26%'><col style='width:22%'><col><thead><tr><th>Emplacement</th><th class='r'>Nœuds</th><th>Cœurs / threads</th><th>Mémoire</th><th>Alias</th></tr></thead><tbody>{site_bars()}</tbody></table> | |
| 262 | +<h3>7b. Par modèle et par puce</h3>{html_table(h_5b, r_5b, 'models', align_right=(1, 2, 3, 4), nowrap=(1, 2, 3, 4))} | |
| 263 | +<div class='keep'><h3>7c. Capacité libre</h3><p class='lead'>{cap_5c[0] if cap_5c else ''}</p> | |
| 264 | +<div class='kpis three'>{kpi('14', 'nœuds LAN libres', '150 cœurs · 332 Go')}{kpi('12', 'Macs loués libres', '114 cœurs · 240 Go')}{kpi('2', 'serveurs OVH libres', 'BHS64b 64 Go · BHS128 128 Go')}</div></div> | |
| 265 | +</section> | |
| 266 | + | |
| 267 | +{sec('8', 'Applications hébergées', f"{tot['apps']} applications, toutes décrites par un manifeste mld (dir, processus, runtimes, RAM, ports, placement, tunnel). Le registre M1M32:~/dispatch/registry.json est la seule source de vérité de « quelle app tourne où » ; état confirmé par maclustr-agentd au moment de la génération.")} | |
| 268 | +{apps_section()} | |
| 269 | +</section> | |
| 270 | + | |
| 271 | +{sec('9', 'Instantané live', f"Mesures de maclustr-agentd le {DATE} à {TIME} : {S.get('nodesOnline', '—')}/{S.get('nodesTotal', '—')} Macs en ligne, CPU moyen {str(S.get('cpuAvg', '—')).replace('.', ',')} %, {fr(S.get('ramUsedGB', 0))} Go de mémoire utilisés sur {fr(int(S.get('ramOnlineGB', 0)))} Go en ligne, {fr(S.get('procs', 0))} processus, {(S.get('apps') or {}).get('up', '—')}/{(S.get('apps') or {}).get('total', '—')} applications saines.")} | |
| 272 | +<table class='t heat'><col style='width:9%'><col style='width:9%'><col style='width:5%'><col style='width:6%'><col style='width:9%'><col style='width:15%'><col style='width:19%'><col style='width:19%'><col style='width:4%'><col><thead><tr><th>Nœud</th><th>Puce</th><th class='r'>Cœurs</th><th class='r'>RAM</th><th>État</th><th>CPU</th><th>Mémoire</th><th>Disque</th><th class='r'>Load</th><th>macOS</th></tr></thead><tbody>{heat_rows()}</tbody></table> | |
| 273 | +</section> | |
| 274 | + | |
| 275 | +{sec('10', 'Sources et méthode')} | |
| 276 | +<p>{sources[0] if sources else ''}</p> | |
| 277 | +<p class='note'>Les identifiants (mots de passe sudo, DeskIn, VNC, jetons) ne figurent pas dans ce dossier : voir <code>NODES.md</code>. Document généré par <code>cluster-skill/dossier/build.py</code> à partir de <code>INVENTAIRE-MACLUSTR.md</code>, de l'inventaire du site www.maclustr.io et de l'API live du cluster ; à régénérer après tout ajout ou retrait de machine.</p> | |
| 278 | +<div class='end'>{LOGO}<span>MacLustr · {DATE}</span></div> | |
| 279 | +</section> | |
| 280 | +</body></html>""" | |
| 281 | + | |
| 282 | +(HERE / "cover.html").write_text(cover) | |
| 283 | +(HERE / "body.html").write_text(body) | |
| 284 | +print("cover.html + body.html écrits —", len(body), "octets") | |
added
dossier/cover.html
+160 −0
@@ -0,0 +1,160 @@ | ||
| 1 | +<!doctype html><html lang='fr'><head><meta charset='utf-8'><title>MacLustr — Dossier technique</title><style>/* Dossier technique MacLustr — feuille d'impression (A4 paysage) */ | |
| 2 | +@page { size: A4 landscape; margin: 13mm 14mm 15mm 14mm; } | |
| 3 | +:root { | |
| 4 | + --ink: #0f1115; --ink-2: #4b5060; --ink-3: #868b99; --line: #dfe2ea; --line-2: #c9cdd8; | |
| 5 | + --paper: #ffffff; --paper-2: #f5f6f9; --paper-3: #eceef4; | |
| 6 | + --accent: #2563eb; --accent-2: #1d4ed8; --accent-dim: #e8effd; --alt: #7c3aed; | |
| 7 | + --good: #16a34a; --bad: #dc2626; --off: #9aa0ae; | |
| 8 | + --sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", Inter, Arial, sans-serif; | |
| 9 | + --mono: "SF Mono", ui-monospace, Menlo, Consolas, monospace; | |
| 10 | +} | |
| 11 | +* { box-sizing: border-box; } | |
| 12 | +html, body { margin: 0; padding: 0; } | |
| 13 | +body { font: 9pt/1.42 var(--sans); color: var(--ink); background: var(--paper); -webkit-print-color-adjust: exact; print-color-adjust: exact; } | |
| 14 | +h1, h2, h3, h4 { margin: 0; letter-spacing: -.02em; line-height: 1.12; } | |
| 15 | +p { margin: 0 0 6px; } | |
| 16 | +code { font: 8.4pt var(--mono); background: var(--paper-2); border: 1px solid var(--line); border-radius: 3px; padding: 0 3px; color: var(--accent-2); } | |
| 17 | +strong { font-weight: 650; } | |
| 18 | +.mono { font-family: var(--mono); } | |
| 19 | +.small { font-size: 8pt; } | |
| 20 | +.faint { color: var(--ink-3); } | |
| 21 | +.r { text-align: right; } | |
| 22 | +.nw { white-space: nowrap; } | |
| 23 | +.logo { width: 22px; height: 22px; color: var(--accent); vertical-align: middle; } | |
| 24 | +.brand { display: inline-flex; align-items: center; gap: 8px; font-weight: 700; font-size: 13pt; letter-spacing: -.02em; } | |
| 25 | +.eyebrow { font-size: 7.6pt; text-transform: uppercase; letter-spacing: .14em; color: var(--ink-3); font-weight: 600; margin: 0 0 8px; } | |
| 26 | + | |
| 27 | +/* ---------- couverture ---------- */ | |
| 28 | +body.cover { background: #09090b; color: #f4f4f5; width: 297mm; height: 210mm; overflow: hidden; } | |
| 29 | +.cover .kpi { background: #111114; border: 0; border-radius: 0; } | |
| 30 | +.cover .kpi b { color: #fff; } .cover .kpi span { color: #a1a1aa; } .cover .kpi small { color: #6b6b76; } | |
| 31 | +.cv { position: relative; width: 297mm; height: 210mm; overflow: hidden; padding: 14mm 18mm; display: flex; flex-direction: column; justify-content: space-between; background: radial-gradient(70% 55% at 80% 0%, rgba(96,165,250,.16), transparent 70%), #09090b; } | |
| 32 | +.cv::after { content: ""; position: absolute; inset: 0; background-image: linear-gradient(rgba(255,255,255,.035) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,.035) 1px, transparent 1px); background-size: 14mm 14mm; -webkit-mask-image: radial-gradient(ellipse at 50% 0%, #000 0%, transparent 75%); mask-image: radial-gradient(ellipse at 50% 0%, #000 0%, transparent 75%); pointer-events: none; } | |
| 33 | +.cv > * { position: relative; z-index: 1; } | |
| 34 | +.cv .brand { color: #f4f4f5; } | |
| 35 | +.cv .logo { color: #60a5fa; } | |
| 36 | +.cv-top { display: flex; justify-content: space-between; align-items: center; color: #a1a1aa; font-size: 9pt; } | |
| 37 | +.cv-mid { max-width: 190mm; } | |
| 38 | +.cv .eyebrow { color: #a1a1aa; } | |
| 39 | +.cv h1 { font-size: 52pt; font-weight: 750; letter-spacing: -.04em; line-height: .96; color: #fff; } | |
| 40 | +.cv h1 span { color: #6b6b76; } | |
| 41 | +.cv .sub { margin-top: 7mm; font-size: 11.5pt; line-height: 1.5; color: #c9cbd3; max-width: 160mm; } | |
| 42 | +.cv-facts { display: grid; grid-template-columns: repeat(6, 1fr); border: 1px solid #26262d; border-radius: 10px; overflow: hidden; background: #111114; } | |
| 43 | +.cv-facts .kpi { padding: 5mm 5mm; border-right: 1px solid #26262d; } | |
| 44 | +.cv-facts .kpi:last-child { border-right: 0; } | |
| 45 | +.cv-facts .kpi b { display: block; font: 600 22pt/1.05 var(--mono); letter-spacing: -.03em; color: #fff; } | |
| 46 | +.cv-facts .kpi span { display: block; color: #a1a1aa; font-size: 8.6pt; margin-top: 2px; } | |
| 47 | +.cv-facts .kpi small { display: block; color: #6b6b76; font: 7.6pt var(--mono); margin-top: 4px; } | |
| 48 | +.cv-bot { display: flex; justify-content: space-between; color: #6b6b76; font-size: 8.4pt; } | |
| 49 | + | |
| 50 | +/* ---------- corps ---------- */ | |
| 51 | +.sec { break-before: page; } | |
| 52 | +.sec:first-child { break-before: auto; } | |
| 53 | +.sec-h { display: flex; align-items: baseline; gap: 12px; border-bottom: 2px solid var(--ink); padding-bottom: 6px; margin-bottom: 10px; } | |
| 54 | +.sec-h .num { font: 600 11pt var(--mono); color: var(--accent); } | |
| 55 | +.sec-h h2 { font-size: 20pt; font-weight: 700; } | |
| 56 | +h3 { font-size: 12pt; font-weight: 650; margin: 10px 0 5px; break-after: avoid; display: flex; align-items: baseline; gap: 8px; } | |
| 57 | +h3 .cnt { font: 500 8.5pt var(--mono); color: var(--ink-3); } | |
| 58 | +h4 { font-size: 9.5pt; font-weight: 650; color: var(--ink-2); text-transform: uppercase; letter-spacing: .08em; margin-bottom: 8px; } | |
| 59 | +.lead { color: var(--ink-2); font-size: 9.1pt; max-width: 245mm; margin-bottom: 4px; } | |
| 60 | +.lead.big { font-size: 12pt; line-height: 1.5; color: var(--ink); margin-bottom: 10px; } | |
| 61 | +.note { color: var(--ink-3); font-size: 8.4pt; margin-top: 4px; } | |
| 62 | +.two { display: grid; grid-template-columns: 1fr 1fr; gap: 10mm; align-items: start; } | |
| 63 | +.three-col { display: grid; grid-template-columns: 1.1fr 1fr 1.2fr; gap: 5mm; align-items: start; } | |
| 64 | +.kpis { display: grid; grid-template-columns: repeat(6, 1fr); gap: 4mm; margin: 6px 0 4px; } | |
| 65 | +.kpis.three { grid-template-columns: repeat(3, 1fr); } | |
| 66 | +.kpi { border: 1px solid var(--line); border-radius: 8px; padding: 7px 10px; background: var(--paper); break-inside: avoid; } | |
| 67 | +.kpi b { display: block; font: 600 17pt/1.05 var(--mono); letter-spacing: -.03em; } | |
| 68 | +.kpi span { display: block; color: var(--ink-2); font-size: 8.4pt; margin-top: 2px; } | |
| 69 | +.kpi small { display: block; color: var(--ink-3); font: 7.4pt var(--mono); margin-top: 4px; } | |
| 70 | +.toc { border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; background: var(--paper-2); } | |
| 71 | +.toc div { display: flex; gap: 10px; padding: 3px 0; border-bottom: 1px dashed var(--line); font-size: 9pt; } | |
| 72 | +.toc div:last-child { border-bottom: 0; } | |
| 73 | +.toc .num { font: 600 8.4pt var(--mono); color: var(--accent); width: 18px; } | |
| 74 | + | |
| 75 | +/* tables */ | |
| 76 | +table.t { width: 100%; border-collapse: collapse; margin: 4px 0 10px; font-size: 8.2pt; break-inside: auto; } | |
| 77 | +table.t th { text-align: left; font-size: 7.4pt; text-transform: uppercase; letter-spacing: .06em; color: var(--ink-3); font-weight: 600; padding: 5px 6px; border-bottom: 1.5px solid var(--ink); white-space: nowrap; } | |
| 78 | +table.t th.r { text-align: right; } | |
| 79 | +table.t td { padding: 4px 6px; border-bottom: 1px solid var(--line); vertical-align: top; overflow-wrap: anywhere; } | |
| 80 | +table.t tr { break-inside: avoid; } | |
| 81 | +table.t thead { display: table-header-group; } | |
| 82 | +table.t td.id { font: 600 8.4pt var(--mono); white-space: nowrap; color: var(--ink); } | |
| 83 | +table.t td.mono, .mono { font-family: var(--mono); } | |
| 84 | +table.t td code { font-size: 7.6pt; } | |
| 85 | +table.t.hw { font-size: 7.5pt; } | |
| 86 | +table.t.hw td { padding: 3px 4px; line-height: 1.3; } | |
| 87 | +table.t.over td:first-child, table.t.models td:first-child { font-weight: 500; } | |
| 88 | +table.t.roles td { font-size: 7.4pt; padding: 3px 5px; line-height: 1.3; } | |
| 89 | +.roles-page { break-before: page; } | |
| 90 | +.keep { break-inside: avoid; } | |
| 91 | +table.t.ovh td { font-size: 7.4pt; line-height: 1.3; } | |
| 92 | +table.t.ovh td:nth-child(6) { min-width: 34mm; } table.t.ovh td code { white-space: normal; word-break: break-all; } | |
| 93 | +.kpis.stack { grid-template-columns: 1fr; gap: 3mm; } | |
| 94 | +table.t.tun td:first-child { font: 600 8pt var(--mono); } | |
| 95 | +table.t.apps td { font-size: 7.8pt; padding: 3px 6px; } | |
| 96 | +.app-group { margin-bottom: 4px; } | |
| 97 | +table.t.heat td, table.t.bars td { vertical-align: middle; } | |
| 98 | +table.t.bars td { padding: 3px 6px; } | |
| 99 | +table.t.bars td small { line-height: 1.1; } | |
| 100 | +td small { color: var(--ink-3); font-size: 7.4pt; } | |
| 101 | + | |
| 102 | +/* barres */ | |
| 103 | +.meter { white-space: nowrap; } | |
| 104 | +.bar { display: inline-block; width: 52%; height: 6px; border-radius: 3px; background: var(--paper-3); vertical-align: middle; overflow: hidden; margin-right: 6px; } | |
| 105 | +.bar i { display: block; height: 100%; background: var(--accent); border-radius: 3px; } | |
| 106 | +.bar.alt i { background: var(--alt); } | |
| 107 | +.meter.wide .bar { width: 58%; } | |
| 108 | +.meter .v { font: 7.8pt var(--mono); color: var(--ink-2); vertical-align: middle; } | |
| 109 | + | |
| 110 | +/* statuts */ | |
| 111 | +.st { display: inline-block; padding: 1px 7px; border-radius: 999px; font-size: 7.2pt; font-weight: 600; border: 1px solid var(--line-2); color: var(--ink-2); white-space: nowrap; } | |
| 112 | +.st::before { content: ""; display: inline-block; width: 5px; height: 5px; border-radius: 50%; background: var(--off); margin-right: 5px; vertical-align: middle; } | |
| 113 | +.st.on { color: #166534; border-color: #bbf7d0; background: #f0fdf4; } .st.on::before { background: var(--good); } | |
| 114 | +.st.bad { color: #991b1b; border-color: #fecaca; background: #fef2f2; } .st.bad::before { background: var(--bad); } | |
| 115 | +.st.off { color: var(--ink-3); background: var(--paper-2); } | |
| 116 | + | |
| 117 | +/* fiches, panneaux */ | |
| 118 | +.fiches p { padding: 6px 9px; border-left: 3px solid var(--accent); background: var(--paper-2); border-radius: 0 6px 6px 0; margin-bottom: 5px; font-size: 8pt; break-inside: avoid; } | |
| 119 | +.panel { border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; margin-bottom: 8px; break-inside: avoid; } | |
| 120 | +.flow { margin: 0; padding: 0; list-style: none; counter-reset: f; } | |
| 121 | +.flow li { display: grid; grid-template-columns: 20px 1fr; gap: 8px; align-items: start; padding: 5px 0; border-bottom: 1px dashed var(--line); font-size: 8.6pt; } | |
| 122 | +.flow li:last-child { border-bottom: 0; } | |
| 123 | +.flow li::before { counter-increment: f; content: counter(f, decimal-leading-zero); font: 600 8pt var(--mono); color: var(--accent); } | |
| 124 | +.flow li b { display: block; font-weight: 650; } | |
| 125 | +.flow li div span { color: var(--ink-2); display: block; } | |
| 126 | +.gw { display: grid; grid-template-columns: 70px 1fr; gap: 2px 10px; padding: 6px 0; border-bottom: 1px dashed var(--line); font-size: 8.6pt; } | |
| 127 | +.gw:last-child { border-bottom: 0; } | |
| 128 | +.gw b { font: 600 9pt var(--mono); grid-row: span 2; align-self: center; } | |
| 129 | +.gw span { color: var(--ink-2); } | |
| 130 | +.gw .mono { font-size: 7.8pt; color: var(--ink-3); } | |
| 131 | + | |
| 132 | +/* carte et sites */ | |
| 133 | +figure.map { margin: 0 0 8px; break-inside: avoid; } | |
| 134 | +figure.map img { width: 100%; height: 78mm; object-fit: cover; object-position: center 40%; border-radius: 8px; border: 1px solid var(--line); display: block; } | |
| 135 | +figure.map figcaption { font-size: 7.8pt; color: var(--ink-3); margin-top: 4px; } | |
| 136 | +.sites { display: grid; grid-template-columns: repeat(4, 1fr); gap: 3mm; margin-top: 6px; } | |
| 137 | +.site { border: 1px solid var(--line); border-radius: 8px; padding: 7px 9px; break-inside: avoid; } | |
| 138 | +.site .s-h { display: flex; justify-content: space-between; align-items: baseline; gap: 6px; font-size: 9.8pt; } | |
| 139 | +.site .s-h .mono { font-size: 8pt; color: var(--ink-2); } | |
| 140 | +.site .s-r { color: var(--ink-3); font-size: 7.8pt; margin: 2px 0 6px; } | |
| 141 | +.site p { font-size: 7.5pt; color: var(--ink-2); min-height: 20px; margin-bottom: 3px; } | |
| 142 | +.site .s-n { display: flex; flex-wrap: wrap; gap: 3px; } | |
| 143 | +.site .s-n span { font: 7.4pt var(--mono); padding: 1px 5px; border-radius: 4px; background: var(--paper-2); border: 1px solid var(--line); } | |
| 144 | + | |
| 145 | +.end { display: flex; align-items: center; gap: 8px; margin-top: 18px; color: var(--ink-3); font: 8.4pt var(--mono); } | |
| 146 | + @page { size: 297mm 210mm; margin: 0; }</style></head> | |
| 147 | +<body class='cover'> | |
| 148 | +<div class='cv'> | |
| 149 | + <div class='cv-top'><div class='brand'><svg class='logo' viewBox='0 0 32 32'><rect x='3' y='3' width='11' height='11' rx='2.5' fill='currentColor'/><rect x='18' y='3' width='11' height='11' rx='2.5' fill='currentColor' opacity='.55'/><rect x='3' y='18' width='11' height='11' rx='2.5' fill='currentColor' opacity='.55'/><rect x='18' y='18' width='11' height='11' rx='2.5' fill='currentColor' opacity='.3'/></svg><span>MacLustr</span></div><span class='mono'>www.maclustr.io</span></div> | |
| 150 | + <div class='cv-mid'> | |
| 151 | + <p class='eyebrow'>Dossier technique · inventaire complet</p> | |
| 152 | + <h1>Un supercalculateur<br><span>fait de Macs.</span></h1> | |
| 153 | + <p class='sub'>33 Macs Apple Silicon et 4 serveurs OVHcloud, répartis dans 7 sites et 6 pays, orchestrés comme une seule plateforme. Nœuds internes, Macs loués, passerelles, tunnel, applications hébergées : tout ce qui compose le cluster au 10 septembre 2026.</p> | |
| 154 | + </div> | |
| 155 | + <div class='cv-facts'> | |
| 156 | + <div class='kpi'><b>37</b><span>nœuds</span><small>33 Macs + 4 Linux</small></div><div class='kpi'><b>484</b><span>cœurs CPU</span><small>398 actifs</small></div><div class='kpi'><b>812</b><span>cœurs GPU</span><small>Apple Silicon</small></div><div class='kpi'><b>1,38 To</b><span>mémoire</span><small>296 Go utilisés</small></div><div class='kpi'><b>36</b><span>applications</span><small>36 saines</small></div><div class='kpi'><b>35</b><span>sites HTTPS</span><small>MacLustr Tunnel</small></div> | |
| 157 | + </div> | |
| 158 | + <div class='cv-bot'><span>Simon-Pierre Boucher · Saint-Augustin-de-Desmaures, Québec</span><span class='mono'>contact@spboucher.ai · état live du 10 septembre 2026 à 23 h 09</span></div> | |
| 159 | +</div> | |
| 160 | +</body></html> | |
| \ No newline at end of file | ||
added
dossier/data.json
+1181 −0
@@ -0,0 +1,1181 @@ | ||
| 1 | +{ | |
| 2 | + "updated": "2026-09-10", | |
| 3 | + "contact": { | |
| 4 | + "email": "contact@spboucher.ai", | |
| 5 | + "owner": "Simon-Pierre Boucher", | |
| 6 | + "site": "https://www.spboucher.ai", | |
| 7 | + "git": "https://git.spboucher.ai" | |
| 8 | + }, | |
| 9 | + "sites": { | |
| 10 | + "staug": { | |
| 11 | + "name": "Saint-Augustin-de-Desmaures", | |
| 12 | + "region": "Québec, Canada", | |
| 13 | + "lat": 46.741, | |
| 14 | + "lon": -71.458, | |
| 15 | + "kind": "lan", | |
| 16 | + "operator": "MacLustr (LAN privé)", | |
| 17 | + "note": "Le cœur du cluster : 21 Macs sur un réseau local 10 GbE, deux NAS UGREEN." | |
| 18 | + }, | |
| 19 | + "bhs": { | |
| 20 | + "name": "Beauharnois", | |
| 21 | + "region": "Québec, Canada", | |
| 22 | + "lat": 45.313, | |
| 23 | + "lon": -73.873, | |
| 24 | + "kind": "ovh", | |
| 25 | + "operator": "OVHcloud BHS8", | |
| 26 | + "note": "Passerelle principale du MacLustr Tunnel et deux serveurs de réserve. ≈ 10 ms du LAN." | |
| 27 | + }, | |
| 28 | + "gra": { | |
| 29 | + "name": "Gravelines", | |
| 30 | + "region": "Hauts-de-France, France", | |
| 31 | + "lat": 50.987, | |
| 32 | + "lon": 2.128, | |
| 33 | + "kind": "ovh", | |
| 34 | + "operator": "OVHcloud GRA1", | |
| 35 | + "note": "Passerelle secondaire : secours et présence européenne." | |
| 36 | + }, | |
| 37 | + "ist": { | |
| 38 | + "name": "Istanbul", | |
| 39 | + "region": "Turquie", | |
| 40 | + "lat": 41.008, | |
| 41 | + "lon": 28.978, | |
| 42 | + "kind": "rented", | |
| 43 | + "operator": "Macly (réseau Erlion)", | |
| 44 | + "note": "Huit Macs dédiés loués : un Mac Studio M1 Max 64 Go et sept Mac mini." | |
| 45 | + }, | |
| 46 | + "atl": { | |
| 47 | + "name": "Atlanta", | |
| 48 | + "region": "Géorgie, États-Unis", | |
| 49 | + "lat": 33.786, | |
| 50 | + "lon": -84.406, | |
| 51 | + "kind": "rented", | |
| 52 | + "operator": "MacStadium", | |
| 53 | + "note": "Mac mini M2 Pro dédié." | |
| 54 | + }, | |
| 55 | + "dub": { | |
| 56 | + "name": "Dublin", | |
| 57 | + "region": "Irlande", | |
| 58 | + "lat": 53.335, | |
| 59 | + "lon": -6.252, | |
| 60 | + "kind": "rented", | |
| 61 | + "operator": "MacStadium", | |
| 62 | + "note": "Deux Mac mini dédiés (M1, M2)." | |
| 63 | + }, | |
| 64 | + "lim": { | |
| 65 | + "name": "Limassol", | |
| 66 | + "region": "Chypre", | |
| 67 | + "lat": 34.687, | |
| 68 | + "lon": 33.037, | |
| 69 | + "kind": "rented", | |
| 70 | + "operator": "rentamac", | |
| 71 | + "note": "Mac mini M4 derrière NAT, joint par la passerelle SSH du fournisseur." | |
| 72 | + } | |
| 73 | + }, | |
| 74 | + "totals": { | |
| 75 | + "macs": 33, | |
| 76 | + "ovh": 4, | |
| 77 | + "apps": 36, | |
| 78 | + "sites": 35 | |
| 79 | + }, | |
| 80 | + "nodes": [ | |
| 81 | + { | |
| 82 | + "id": "M3U96a", | |
| 83 | + "group": "lan", | |
| 84 | + "site": "staug", | |
| 85 | + "model": "Mac Studio", | |
| 86 | + "chip": "M3 Ultra", | |
| 87 | + "tier": "ultra", | |
| 88 | + "cores": 32, | |
| 89 | + "gpu": 80, | |
| 90 | + "ram": 96, | |
| 91 | + "disk": 926, | |
| 92 | + "ip": "192.168.2.87", | |
| 93 | + "role": "Nœud maître de calcul et d'applications", | |
| 94 | + "desc": "Le plus gros nœud du cluster. Héberge la console Administration·Ka et une grappe d'applications Next.js/FastAPI (QC26, RareIndex, Fetcha, PolyLLM, UQO).", | |
| 95 | + "tags": [ | |
| 96 | + "worker", | |
| 97 | + "apps", | |
| 98 | + "calcul" | |
| 99 | + ] | |
| 100 | + }, | |
| 101 | + { | |
| 102 | + "id": "M3U96b", | |
| 103 | + "group": "lan", | |
| 104 | + "site": "staug", | |
| 105 | + "model": "Mac Studio", | |
| 106 | + "chip": "M3 Ultra", | |
| 107 | + "tier": "ultra", | |
| 108 | + "cores": 32, | |
| 109 | + "gpu": 80, | |
| 110 | + "ram": 96, | |
| 111 | + "disk": 926, | |
| 112 | + "ip": "192.168.2.82", | |
| 113 | + "role": "Réservé à HF Market Data", | |
| 114 | + "desc": "Sert le lac de données FirstRate (353 Go de séries intraday, DuckDB) derrière www.hfmarketdata.io. Réservé : aucune autre app n'y est placée.", | |
| 115 | + "tags": [ | |
| 116 | + "worker", | |
| 117 | + "data", | |
| 118 | + "réservé" | |
| 119 | + ] | |
| 120 | + }, | |
| 121 | + { | |
| 122 | + "id": "M2U64", | |
| 123 | + "group": "lan", | |
| 124 | + "site": "staug", | |
| 125 | + "model": "Mac Studio", | |
| 126 | + "chip": "M2 Ultra", | |
| 127 | + "tier": "ultra", | |
| 128 | + "cores": 24, | |
| 129 | + "gpu": 76, | |
| 130 | + "ram": 64, | |
| 131 | + "disk": 926, | |
| 132 | + "ip": "192.168.2.107", | |
| 133 | + "role": "Nœud d'applications sans session graphique", | |
| 134 | + "desc": "Tourne en LaunchDaemon (headless). Héberge SPB Cloud, vQuant, UQO Éval, Food·Ka et les consoles MacLustr.", | |
| 135 | + "tags": [ | |
| 136 | + "worker", | |
| 137 | + "apps", | |
| 138 | + "headless" | |
| 139 | + ] | |
| 140 | + }, | |
| 141 | + { | |
| 142 | + "id": "M4M64a", | |
| 143 | + "group": "lan", | |
| 144 | + "site": "staug", | |
| 145 | + "model": "Mac Studio", | |
| 146 | + "chip": "M4 Max", | |
| 147 | + "tier": "max", | |
| 148 | + "cores": 16, | |
| 149 | + "gpu": 40, | |
| 150 | + "ram": 64, | |
| 151 | + "disk": 926, | |
| 152 | + "ip": "192.168.2.83", | |
| 153 | + "role": "Applications + agent de supervision", | |
| 154 | + "desc": "Héberge maclustr-agentd (:9210), la source des apps macOS et iOS MacLustr, ce site www.maclustr.io, ainsi que Lou·Ka, Fabri·Ka, Ka·Stats, UQO-Chat et PDB API.", | |
| 155 | + "tags": [ | |
| 156 | + "worker", | |
| 157 | + "apps", | |
| 158 | + "monitoring" | |
| 159 | + ] | |
| 160 | + }, | |
| 161 | + { | |
| 162 | + "id": "M4M64b", | |
| 163 | + "group": "lan", | |
| 164 | + "site": "staug", | |
| 165 | + "model": "Mac Studio", | |
| 166 | + "chip": "M4 Max", | |
| 167 | + "tier": "max", | |
| 168 | + "cores": 16, | |
| 169 | + "gpu": 40, | |
| 170 | + "ram": 64, | |
| 171 | + "disk": 926, | |
| 172 | + "ip": "192.168.2.78", | |
| 173 | + "role": "Applications Groupe Ka + index", | |
| 174 | + "desc": "API·Ka (Postgres 17 + Redis), Groupe Ka, Immo·Ka, Auto·Ka, CancerIndex, WebSensor, UQO IMM1003.", | |
| 175 | + "tags": [ | |
| 176 | + "worker", | |
| 177 | + "apps", | |
| 178 | + "postgres" | |
| 179 | + ] | |
| 180 | + }, | |
| 181 | + { | |
| 182 | + "id": "M4BP48", | |
| 183 | + "group": "lan", | |
| 184 | + "site": "staug", | |
| 185 | + "model": "MacBook Pro", | |
| 186 | + "chip": "M4 Max", | |
| 187 | + "tier": "max", | |
| 188 | + "cores": 16, | |
| 189 | + "gpu": 40, | |
| 190 | + "ram": 48, | |
| 191 | + "disk": 926, | |
| 192 | + "ip": "192.168.2.80", | |
| 193 | + "role": "Nœud de calcul", | |
| 194 | + "desc": "Portable intégré au cluster ; disponible pour les jobs distribués (uv, Python 3.12).", | |
| 195 | + "tags": [ | |
| 196 | + "worker", | |
| 197 | + "calcul" | |
| 198 | + ] | |
| 199 | + }, | |
| 200 | + { | |
| 201 | + "id": "M4BP36", | |
| 202 | + "group": "lan", | |
| 203 | + "site": "staug", | |
| 204 | + "model": "MacBook Pro", | |
| 205 | + "chip": "M4 Max", | |
| 206 | + "tier": "max", | |
| 207 | + "cores": 14, | |
| 208 | + "gpu": 32, | |
| 209 | + "ram": 36, | |
| 210 | + "disk": null, | |
| 211 | + "ip": "192.168.2.133", | |
| 212 | + "role": "Nœud de calcul (mobile)", | |
| 213 | + "desc": "Portable, souvent hors ligne : nœud d'appoint pour le calcul distribué.", | |
| 214 | + "tags": [ | |
| 215 | + "worker", | |
| 216 | + "calcul" | |
| 217 | + ] | |
| 218 | + }, | |
| 219 | + { | |
| 220 | + "id": "M4M36", | |
| 221 | + "group": "lan", | |
| 222 | + "site": "staug", | |
| 223 | + "model": "Mac Studio", | |
| 224 | + "chip": "M4 Max", | |
| 225 | + "tier": "max", | |
| 226 | + "cores": 14, | |
| 227 | + "gpu": 40, | |
| 228 | + "ram": 36, | |
| 229 | + "disk": 460, | |
| 230 | + "ip": "192.168.2.69", | |
| 231 | + "role": "Gardiens Ka", | |
| 232 | + "desc": "Héberge les trois agents gardiens ka2, ka4 et ka6 qui surveillent, réparent et poussent les apps Groupe Ka.", | |
| 233 | + "tags": [ | |
| 234 | + "worker", | |
| 235 | + "agents" | |
| 236 | + ] | |
| 237 | + }, | |
| 238 | + { | |
| 239 | + "id": "M2M32", | |
| 240 | + "group": "lan", | |
| 241 | + "site": "staug", | |
| 242 | + "model": "Mac Studio", | |
| 243 | + "chip": "M2 Max", | |
| 244 | + "tier": "max", | |
| 245 | + "cores": 12, | |
| 246 | + "gpu": 38, | |
| 247 | + "ram": 32, | |
| 248 | + "disk": 460, | |
| 249 | + "ip": "192.168.2.90", | |
| 250 | + "role": "Nœud de calcul", | |
| 251 | + "desc": "Docker, Node, Python : nœud générique pour le calcul et les déploiements mld.", | |
| 252 | + "tags": [ | |
| 253 | + "worker", | |
| 254 | + "calcul" | |
| 255 | + ] | |
| 256 | + }, | |
| 257 | + { | |
| 258 | + "id": "M2M32b", | |
| 259 | + "group": "lan", | |
| 260 | + "site": "staug", | |
| 261 | + "model": "Mac Studio", | |
| 262 | + "chip": "M2 Max", | |
| 263 | + "tier": "max", | |
| 264 | + "cores": 12, | |
| 265 | + "gpu": 38, | |
| 266 | + "ram": 32, | |
| 267 | + "disk": 460, | |
| 268 | + "ip": "192.168.2.77", | |
| 269 | + "role": "Nœud de calcul", | |
| 270 | + "desc": "Nœud générique, ex-flotte de scraping Trouve-Ka.", | |
| 271 | + "tags": [ | |
| 272 | + "worker", | |
| 273 | + "calcul" | |
| 274 | + ] | |
| 275 | + }, | |
| 276 | + { | |
| 277 | + "id": "M2M32c", | |
| 278 | + "group": "lan", | |
| 279 | + "site": "staug", | |
| 280 | + "model": "Mac Studio", | |
| 281 | + "chip": "M2 Max", | |
| 282 | + "tier": "max", | |
| 283 | + "cores": 12, | |
| 284 | + "gpu": 30, | |
| 285 | + "ram": 32, | |
| 286 | + "disk": 460, | |
| 287 | + "ip": "192.168.2.89", | |
| 288 | + "role": "Nœud de calcul", | |
| 289 | + "desc": "Nœud générique pour les jobs Python distribués.", | |
| 290 | + "tags": [ | |
| 291 | + "worker", | |
| 292 | + "calcul" | |
| 293 | + ] | |
| 294 | + }, | |
| 295 | + { | |
| 296 | + "id": "m4mc", | |
| 297 | + "group": "lan", | |
| 298 | + "site": "staug", | |
| 299 | + "model": "Mac mini", | |
| 300 | + "chip": "M4 Pro", | |
| 301 | + "tier": "pro", | |
| 302 | + "cores": 12, | |
| 303 | + "gpu": 16, | |
| 304 | + "ram": 24, | |
| 305 | + "disk": 460, | |
| 306 | + "ip": "192.168.2.75", | |
| 307 | + "role": "Nœud de calcul", | |
| 308 | + "desc": "Mac mini M4 Pro, nœud générique.", | |
| 309 | + "tags": [ | |
| 310 | + "worker", | |
| 311 | + "calcul" | |
| 312 | + ] | |
| 313 | + }, | |
| 314 | + { | |
| 315 | + "id": "M1M32", | |
| 316 | + "group": "lan", | |
| 317 | + "site": "staug", | |
| 318 | + "model": "Mac Studio", | |
| 319 | + "chip": "M1 Max", | |
| 320 | + "tier": "max", | |
| 321 | + "cores": 10, | |
| 322 | + "gpu": 32, | |
| 323 | + "ram": 32, | |
| 324 | + "disk": 460, | |
| 325 | + "ip": "192.168.2.76", | |
| 326 | + "role": "Passerelle du cluster", | |
| 327 | + "desc": "Porte d'entrée : héberge spbgit (git.spboucher.ai, dépôts bare de toutes les apps) et l'orchestrateur mld qui place, déploie, migre et répare chaque application. Aucune app métier n'y tourne.", | |
| 328 | + "tags": [ | |
| 329 | + "gateway", | |
| 330 | + "git", | |
| 331 | + "orchestrateur" | |
| 332 | + ] | |
| 333 | + }, | |
| 334 | + { | |
| 335 | + "id": "m4ma", | |
| 336 | + "group": "lan", | |
| 337 | + "site": "staug", | |
| 338 | + "model": "Mac mini", | |
| 339 | + "chip": "M4", | |
| 340 | + "tier": "base", | |
| 341 | + "cores": 10, | |
| 342 | + "gpu": 10, | |
| 343 | + "ram": 24, | |
| 344 | + "disk": 460, | |
| 345 | + "ip": "192.168.2.73", | |
| 346 | + "role": "Nœud de calcul", | |
| 347 | + "desc": "Mac mini M4, nœud générique.", | |
| 348 | + "tags": [ | |
| 349 | + "worker" | |
| 350 | + ] | |
| 351 | + }, | |
| 352 | + { | |
| 353 | + "id": "m4mb", | |
| 354 | + "group": "lan", | |
| 355 | + "site": "staug", | |
| 356 | + "model": "Mac mini", | |
| 357 | + "chip": "M4", | |
| 358 | + "tier": "base", | |
| 359 | + "cores": 10, | |
| 360 | + "gpu": 10, | |
| 361 | + "ram": 16, | |
| 362 | + "disk": 228, | |
| 363 | + "ip": "192.168.2.74", | |
| 364 | + "role": "Nœud de calcul", | |
| 365 | + "desc": "Mac mini M4, nœud générique.", | |
| 366 | + "tags": [ | |
| 367 | + "worker" | |
| 368 | + ] | |
| 369 | + }, | |
| 370 | + { | |
| 371 | + "id": "m4md", | |
| 372 | + "group": "lan", | |
| 373 | + "site": "staug", | |
| 374 | + "model": "Mac mini", | |
| 375 | + "chip": "M4", | |
| 376 | + "tier": "base", | |
| 377 | + "cores": 10, | |
| 378 | + "gpu": 10, | |
| 379 | + "ram": 16, | |
| 380 | + "disk": 228, | |
| 381 | + "ip": "192.168.2.110", | |
| 382 | + "role": "Nœud de calcul", | |
| 383 | + "desc": "Mac mini M4 ajouté le 4 septembre 2026.", | |
| 384 | + "tags": [ | |
| 385 | + "worker" | |
| 386 | + ] | |
| 387 | + }, | |
| 388 | + { | |
| 389 | + "id": "m2m16", | |
| 390 | + "group": "lan", | |
| 391 | + "site": "staug", | |
| 392 | + "model": "Mac mini", | |
| 393 | + "chip": "M2 Pro", | |
| 394 | + "tier": "pro", | |
| 395 | + "cores": 10, | |
| 396 | + "gpu": 16, | |
| 397 | + "ram": 16, | |
| 398 | + "disk": 460, | |
| 399 | + "ip": "192.168.2.85", | |
| 400 | + "role": "Nœud de calcul", | |
| 401 | + "desc": "Mac mini M2 Pro, nœud générique.", | |
| 402 | + "tags": [ | |
| 403 | + "worker" | |
| 404 | + ] | |
| 405 | + }, | |
| 406 | + { | |
| 407 | + "id": "M3BA24", | |
| 408 | + "group": "lan", | |
| 409 | + "site": "staug", | |
| 410 | + "model": "MacBook Air", | |
| 411 | + "chip": "M3", | |
| 412 | + "tier": "base", | |
| 413 | + "cores": 8, | |
| 414 | + "gpu": 10, | |
| 415 | + "ram": 24, | |
| 416 | + "disk": 460, | |
| 417 | + "ip": "192.168.2.81", | |
| 418 | + "role": "Nœud d'appoint", | |
| 419 | + "desc": "MacBook Air, nœud d'appoint pour le calcul léger.", | |
| 420 | + "tags": [ | |
| 421 | + "worker" | |
| 422 | + ] | |
| 423 | + }, | |
| 424 | + { | |
| 425 | + "id": "M3BA16", | |
| 426 | + "group": "lan", | |
| 427 | + "site": "staug", | |
| 428 | + "model": "MacBook Air", | |
| 429 | + "chip": "M3", | |
| 430 | + "tier": "base", | |
| 431 | + "cores": 8, | |
| 432 | + "gpu": 10, | |
| 433 | + "ram": 16, | |
| 434 | + "disk": 460, | |
| 435 | + "ip": "192.168.2.70", | |
| 436 | + "role": "Nœud d'appoint", | |
| 437 | + "desc": "MacBook Air, nœud d'appoint.", | |
| 438 | + "tags": [ | |
| 439 | + "worker" | |
| 440 | + ] | |
| 441 | + }, | |
| 442 | + { | |
| 443 | + "id": "m2m8a", | |
| 444 | + "group": "lan", | |
| 445 | + "site": "staug", | |
| 446 | + "model": "Mac mini", | |
| 447 | + "chip": "M2", | |
| 448 | + "tier": "base", | |
| 449 | + "cores": 8, | |
| 450 | + "gpu": 10, | |
| 451 | + "ram": 8, | |
| 452 | + "disk": 460, | |
| 453 | + "ip": "192.168.2.84", | |
| 454 | + "role": "Nœud léger", | |
| 455 | + "desc": "Mac mini M2 8 Go : tâches légères et tests.", | |
| 456 | + "tags": [ | |
| 457 | + "worker" | |
| 458 | + ] | |
| 459 | + }, | |
| 460 | + { | |
| 461 | + "id": "m2m8b", | |
| 462 | + "group": "lan", | |
| 463 | + "site": "staug", | |
| 464 | + "model": "Mac mini", | |
| 465 | + "chip": "M2", | |
| 466 | + "tier": "base", | |
| 467 | + "cores": 8, | |
| 468 | + "gpu": 10, | |
| 469 | + "ram": 8, | |
| 470 | + "disk": 228, | |
| 471 | + "ip": "192.168.2.72", | |
| 472 | + "role": "Nœud léger", | |
| 473 | + "desc": "Mac mini M2 8 Go : tâches légères et tests.", | |
| 474 | + "tags": [ | |
| 475 | + "worker" | |
| 476 | + ] | |
| 477 | + }, | |
| 478 | + { | |
| 479 | + "id": "M1M64", | |
| 480 | + "group": "rented", | |
| 481 | + "model": "Mac Studio", | |
| 482 | + "chip": "M1 Max", | |
| 483 | + "tier": "max", | |
| 484 | + "cores": 10, | |
| 485 | + "gpu": 32, | |
| 486 | + "ram": 64, | |
| 487 | + "disk": 1800, | |
| 488 | + "site": "ist", | |
| 489 | + "provider": "Macly", | |
| 490 | + "location": "Istanbul, Turquie", | |
| 491 | + "role": "Nœud distant à grande mémoire", | |
| 492 | + "desc": "Mac Studio loué, 64 Go et 1,8 To : réservé aux déploiements explicites (mld deploy --node M1M64).", | |
| 493 | + "tags": [ | |
| 494 | + "distant", | |
| 495 | + "réservé" | |
| 496 | + ] | |
| 497 | + }, | |
| 498 | + { | |
| 499 | + "id": "m2m16b", | |
| 500 | + "group": "rented", | |
| 501 | + "model": "Mac mini", | |
| 502 | + "chip": "M2 Pro", | |
| 503 | + "tier": "pro", | |
| 504 | + "cores": 10, | |
| 505 | + "gpu": 16, | |
| 506 | + "ram": 16, | |
| 507 | + "disk": 926, | |
| 508 | + "site": "atl", | |
| 509 | + "provider": "MacStadium", | |
| 510 | + "location": "Atlanta, États-Unis", | |
| 511 | + "role": "Nœud distant", | |
| 512 | + "desc": "Mac mini M2 Pro dédié, raccordé au tunnel MacLustr.", | |
| 513 | + "tags": [ | |
| 514 | + "distant", | |
| 515 | + "réservé" | |
| 516 | + ] | |
| 517 | + }, | |
| 518 | + { | |
| 519 | + "id": "m1m16", | |
| 520 | + "group": "rented", | |
| 521 | + "model": "Mac mini", | |
| 522 | + "chip": "M1", | |
| 523 | + "tier": "base", | |
| 524 | + "cores": 8, | |
| 525 | + "gpu": 8, | |
| 526 | + "ram": 16, | |
| 527 | + "disk": 926, | |
| 528 | + "site": "dub", | |
| 529 | + "provider": "MacStadium", | |
| 530 | + "location": "Dublin, Irlande", | |
| 531 | + "role": "Nœud distant", | |
| 532 | + "desc": "Mac mini M1 dédié.", | |
| 533 | + "tags": [ | |
| 534 | + "distant", | |
| 535 | + "réservé" | |
| 536 | + ] | |
| 537 | + }, | |
| 538 | + { | |
| 539 | + "id": "m2m16c", | |
| 540 | + "group": "rented", | |
| 541 | + "model": "Mac mini", | |
| 542 | + "chip": "M2", | |
| 543 | + "tier": "base", | |
| 544 | + "cores": 8, | |
| 545 | + "gpu": 10, | |
| 546 | + "ram": 16, | |
| 547 | + "disk": 926, | |
| 548 | + "site": "dub", | |
| 549 | + "provider": "MacStadium", | |
| 550 | + "location": "Dublin, Irlande", | |
| 551 | + "role": "Nœud distant", | |
| 552 | + "desc": "Mac mini M2 dédié.", | |
| 553 | + "tags": [ | |
| 554 | + "distant", | |
| 555 | + "réservé" | |
| 556 | + ] | |
| 557 | + }, | |
| 558 | + { | |
| 559 | + "id": "m4me", | |
| 560 | + "group": "rented", | |
| 561 | + "model": "Mac mini", | |
| 562 | + "chip": "M4", | |
| 563 | + "tier": "base", | |
| 564 | + "cores": 10, | |
| 565 | + "gpu": 10, | |
| 566 | + "ram": 16, | |
| 567 | + "disk": 228, | |
| 568 | + "site": "ist", | |
| 569 | + "provider": "Macly", | |
| 570 | + "location": "Istanbul, Turquie", | |
| 571 | + "role": "Nœud distant", | |
| 572 | + "desc": "Mac mini M4 dédié, amorcé à distance (DeskIn + script), PM2 en LaunchDaemon.", | |
| 573 | + "tags": [ | |
| 574 | + "distant", | |
| 575 | + "réservé" | |
| 576 | + ] | |
| 577 | + }, | |
| 578 | + { | |
| 579 | + "id": "m4mf", | |
| 580 | + "group": "rented", | |
| 581 | + "model": "Mac mini", | |
| 582 | + "chip": "M4", | |
| 583 | + "tier": "base", | |
| 584 | + "cores": 10, | |
| 585 | + "gpu": 10, | |
| 586 | + "ram": 16, | |
| 587 | + "disk": 228, | |
| 588 | + "site": "ist", | |
| 589 | + "provider": "Macly", | |
| 590 | + "location": "Istanbul, Turquie", | |
| 591 | + "role": "Nœud distant", | |
| 592 | + "desc": "Jumeau de m4me.", | |
| 593 | + "tags": [ | |
| 594 | + "distant", | |
| 595 | + "réservé" | |
| 596 | + ] | |
| 597 | + }, | |
| 598 | + { | |
| 599 | + "id": "m4mg", | |
| 600 | + "group": "rented", | |
| 601 | + "model": "Mac mini", | |
| 602 | + "chip": "M4", | |
| 603 | + "tier": "base", | |
| 604 | + "cores": 10, | |
| 605 | + "gpu": 10, | |
| 606 | + "ram": 16, | |
| 607 | + "disk": 228, | |
| 608 | + "site": "lim", | |
| 609 | + "provider": "rentamac", | |
| 610 | + "location": "Limassol, Chypre", | |
| 611 | + "role": "Nœud distant (Méditerranée)", | |
| 612 | + "desc": "Derrière NAT, joint par la passerelle SSH du fournisseur. Xcode complet préinstallé.", | |
| 613 | + "tags": [ | |
| 614 | + "distant", | |
| 615 | + "réservé", | |
| 616 | + "europe" | |
| 617 | + ] | |
| 618 | + }, | |
| 619 | + { | |
| 620 | + "id": "m4mh", | |
| 621 | + "group": "rented", | |
| 622 | + "model": "Mac mini", | |
| 623 | + "chip": "M4", | |
| 624 | + "tier": "base", | |
| 625 | + "cores": 10, | |
| 626 | + "gpu": 10, | |
| 627 | + "ram": 16, | |
| 628 | + "disk": 228, | |
| 629 | + "site": "ist", | |
| 630 | + "provider": "Macly", | |
| 631 | + "location": "Istanbul, Turquie", | |
| 632 | + "role": "Nœud distant", | |
| 633 | + "desc": "Mac mini M4 dédié, ajouté le 10 septembre 2026.", | |
| 634 | + "tags": [ | |
| 635 | + "distant", | |
| 636 | + "réservé" | |
| 637 | + ] | |
| 638 | + }, | |
| 639 | + { | |
| 640 | + "id": "m1m16b", | |
| 641 | + "group": "rented", | |
| 642 | + "model": "Mac mini", | |
| 643 | + "chip": "M1", | |
| 644 | + "tier": "base", | |
| 645 | + "cores": 8, | |
| 646 | + "gpu": 8, | |
| 647 | + "ram": 16, | |
| 648 | + "disk": 228, | |
| 649 | + "site": "ist", | |
| 650 | + "provider": "Macly", | |
| 651 | + "location": "Istanbul, Turquie", | |
| 652 | + "role": "Nœud distant", | |
| 653 | + "desc": "Mac mini M1 dédié, ajouté le 10 septembre 2026.", | |
| 654 | + "tags": [ | |
| 655 | + "distant", | |
| 656 | + "réservé" | |
| 657 | + ] | |
| 658 | + }, | |
| 659 | + { | |
| 660 | + "id": "m4mi", | |
| 661 | + "group": "rented", | |
| 662 | + "model": "Mac mini", | |
| 663 | + "chip": "M4", | |
| 664 | + "tier": "base", | |
| 665 | + "cores": 10, | |
| 666 | + "gpu": 10, | |
| 667 | + "ram": 16, | |
| 668 | + "disk": 228, | |
| 669 | + "site": "ist", | |
| 670 | + "provider": "Macly", | |
| 671 | + "location": "Istanbul, Turquie", | |
| 672 | + "role": "Nœud distant", | |
| 673 | + "desc": "Mac mini M4 dédié, ajouté le 10 septembre 2026.", | |
| 674 | + "tags": [ | |
| 675 | + "distant", | |
| 676 | + "réservé" | |
| 677 | + ] | |
| 678 | + }, | |
| 679 | + { | |
| 680 | + "id": "m4mj", | |
| 681 | + "group": "rented", | |
| 682 | + "model": "Mac mini", | |
| 683 | + "chip": "M4", | |
| 684 | + "tier": "base", | |
| 685 | + "cores": 10, | |
| 686 | + "gpu": 10, | |
| 687 | + "ram": 16, | |
| 688 | + "disk": 228, | |
| 689 | + "site": "ist", | |
| 690 | + "provider": "Macly", | |
| 691 | + "location": "Istanbul, Turquie", | |
| 692 | + "role": "Nœud distant", | |
| 693 | + "desc": "Mac mini M4 dédié, ajouté le 10 septembre 2026.", | |
| 694 | + "tags": [ | |
| 695 | + "distant", | |
| 696 | + "réservé" | |
| 697 | + ] | |
| 698 | + }, | |
| 699 | + { | |
| 700 | + "id": "m4mk", | |
| 701 | + "group": "rented", | |
| 702 | + "model": "Mac mini", | |
| 703 | + "chip": "M4", | |
| 704 | + "tier": "base", | |
| 705 | + "cores": 10, | |
| 706 | + "gpu": 10, | |
| 707 | + "ram": 16, | |
| 708 | + "disk": 228, | |
| 709 | + "site": "ist", | |
| 710 | + "provider": "Macly", | |
| 711 | + "location": "Istanbul, Turquie", | |
| 712 | + "role": "Nœud distant", | |
| 713 | + "desc": "Mac mini M4 dédié, ajouté le 10 septembre 2026.", | |
| 714 | + "tags": [ | |
| 715 | + "distant", | |
| 716 | + "réservé" | |
| 717 | + ] | |
| 718 | + }, | |
| 719 | + { | |
| 720 | + "id": "BHS64", | |
| 721 | + "group": "ovh", | |
| 722 | + "model": "OVH ADVANCE-2", | |
| 723 | + "chip": "AMD EPYC 4345P", | |
| 724 | + "tier": "epyc", | |
| 725 | + "cores": 16, | |
| 726 | + "threads": true, | |
| 727 | + "gpu": 0, | |
| 728 | + "ram": 64, | |
| 729 | + "disk": 960, | |
| 730 | + "site": "bhs", | |
| 731 | + "provider": "OVHcloud", | |
| 732 | + "location": "Beauharnois, Québec", | |
| 733 | + "role": "Passerelle principale MacLustr Tunnel", | |
| 734 | + "desc": "Point d'entrée public de tous les sites : hub WireGuard 10.67.0.1 + Caddy (TLS Let's Encrypt automatique). EPYC 4345P 8 c/16 t, 64 Go DDR5 ECC, 2 × 960 Go NVMe RAID 1, 3 Gbit/s, Ubuntu 24.04. ≈ 10 ms depuis le cluster.", | |
| 735 | + "tags": [ | |
| 736 | + "gateway", | |
| 737 | + "wireguard", | |
| 738 | + "caddy", | |
| 739 | + "linux" | |
| 740 | + ] | |
| 741 | + }, | |
| 742 | + { | |
| 743 | + "id": "BHS64b", | |
| 744 | + "group": "ovh", | |
| 745 | + "model": "OVH ADVANCE-2", | |
| 746 | + "chip": "AMD EPYC 4345P", | |
| 747 | + "tier": "epyc", | |
| 748 | + "cores": 16, | |
| 749 | + "threads": true, | |
| 750 | + "gpu": 0, | |
| 751 | + "ram": 64, | |
| 752 | + "disk": 960, | |
| 753 | + "site": "bhs", | |
| 754 | + "provider": "OVHcloud", | |
| 755 | + "location": "Beauharnois, Québec", | |
| 756 | + "role": "Serveur de réserve", | |
| 757 | + "desc": "Jumeau de BHS64 : EPYC 4345P 8 c/16 t, 64 Go DDR5 ECC, 2 × 960 Go NVMe RAID 1, 3 Gbit/s, Ubuntu 24.04. Réservé pour un usage à venir, hors tunnel.", | |
| 758 | + "tags": [ | |
| 759 | + "linux", | |
| 760 | + "réserve" | |
| 761 | + ] | |
| 762 | + }, | |
| 763 | + { | |
| 764 | + "id": "BHS128", | |
| 765 | + "group": "ovh", | |
| 766 | + "model": "OVH ADVANCE-1", | |
| 767 | + "chip": "AMD EPYC 4245P", | |
| 768 | + "tier": "epyc", | |
| 769 | + "cores": 12, | |
| 770 | + "threads": true, | |
| 771 | + "gpu": 0, | |
| 772 | + "ram": 128, | |
| 773 | + "disk": 960, | |
| 774 | + "site": "bhs", | |
| 775 | + "provider": "OVHcloud", | |
| 776 | + "location": "Beauharnois, Québec", | |
| 777 | + "role": "Serveur de réserve à grande mémoire", | |
| 778 | + "desc": "EPYC 4245P 6 c/12 t, 128 Go DDR5, 2 × 960 Go NVMe RAID 1, 3 Gbit/s, Ubuntu 24.04. Prêt à accueillir une application ou une base de données exigeante ; hors tunnel.", | |
| 779 | + "tags": [ | |
| 780 | + "linux", | |
| 781 | + "réserve", | |
| 782 | + "128 Go" | |
| 783 | + ] | |
| 784 | + }, | |
| 785 | + { | |
| 786 | + "id": "R9128", | |
| 787 | + "group": "ovh", | |
| 788 | + "model": "OVH RISE-L", | |
| 789 | + "chip": "AMD Ryzen 9 9950X", | |
| 790 | + "tier": "ryzen", | |
| 791 | + "cores": 32, | |
| 792 | + "threads": true, | |
| 793 | + "gpu": 0, | |
| 794 | + "ram": 128, | |
| 795 | + "disk": 960, | |
| 796 | + "site": "gra", | |
| 797 | + "provider": "OVHcloud", | |
| 798 | + "location": "Gravelines, France", | |
| 799 | + "role": "Passerelle secondaire (Europe)", | |
| 800 | + "desc": "Hub WireGuard 10.66.0.1 + Caddy : secours et présence européenne du tunnel. Ryzen 9 9950X 16 c/32 t, 128 Go DDR5, 2 × 960 Go NVMe RAID 1, 1 Gbit/s, Ubuntu 24.04.", | |
| 801 | + "tags": [ | |
| 802 | + "gateway", | |
| 803 | + "wireguard", | |
| 804 | + "caddy", | |
| 805 | + "linux", | |
| 806 | + "europe" | |
| 807 | + ] | |
| 808 | + } | |
| 809 | + ], | |
| 810 | + "appGroups": [ | |
| 811 | + { | |
| 812 | + "id": "ka", | |
| 813 | + "name": "Groupe Ka", | |
| 814 | + "blurb": "L'écosystème immobilier, automobile, alimentaire et emploi du Québec : une famille de plateformes qui partagent identité (KA ID), données (API·Ka) et gardiens autonomes." | |
| 815 | + }, | |
| 816 | + { | |
| 817 | + "id": "data", | |
| 818 | + "name": "Données & intelligence", | |
| 819 | + "blurb": "Indices, marchés et infrastructures de données servis en API publiques." | |
| 820 | + }, | |
| 821 | + { | |
| 822 | + "id": "uqo", | |
| 823 | + "name": "Enseignement UQO", | |
| 824 | + "blurb": "Outils de cours interactifs, tuteur IA et ateliers d'évaluation immobilière." | |
| 825 | + }, | |
| 826 | + { | |
| 827 | + "id": "platform", | |
| 828 | + "name": "Plateforme MacLustr", | |
| 829 | + "blurb": "Les services qui font tourner le cluster lui-même." | |
| 830 | + }, | |
| 831 | + { | |
| 832 | + "id": "spb", | |
| 833 | + "name": "SPB", | |
| 834 | + "blurb": "Sites et services personnels." | |
| 835 | + } | |
| 836 | + ], | |
| 837 | + "apps": [ | |
| 838 | + { | |
| 839 | + "id": "groupe-ka", | |
| 840 | + "group": "ka", | |
| 841 | + "name": "Groupe Ka", | |
| 842 | + "domain": "www.groupe-ka.com", | |
| 843 | + "node": "M4M64b", | |
| 844 | + "port": 8110, | |
| 845 | + "desc": "Portail du groupe : vision, univers, dossier investisseurs." | |
| 846 | + }, | |
| 847 | + { | |
| 848 | + "id": "lou-ka", | |
| 849 | + "group": "ka", | |
| 850 | + "name": "Lou·Ka", | |
| 851 | + "domain": "www.lou-ka.com", | |
| 852 | + "node": "M4M64a", | |
| 853 | + "port": 8095, | |
| 854 | + "desc": "Location résidentielle : annonces, carte 3D, Lou-Ka Score." | |
| 855 | + }, | |
| 856 | + { | |
| 857 | + "id": "immo-ka", | |
| 858 | + "group": "ka", | |
| 859 | + "name": "Immo·Ka", | |
| 860 | + "domain": "www.immo-ka.com", | |
| 861 | + "node": "M4M64b", | |
| 862 | + "port": 8096, | |
| 863 | + "desc": "Propriétés à vendre : 70 000+ annonces, analyses de prix, KA Scores." | |
| 864 | + }, | |
| 865 | + { | |
| 866 | + "id": "vrai-prix", | |
| 867 | + "group": "ka", | |
| 868 | + "name": "Vrai-Prix", | |
| 869 | + "domain": "www.vrai-prix.com", | |
| 870 | + "node": "M3U96a", | |
| 871 | + "port": 8090, | |
| 872 | + "desc": "Évaluation hédonique, méthode du coût, statistiques de marché." | |
| 873 | + }, | |
| 874 | + { | |
| 875 | + "id": "auto-ka", | |
| 876 | + "group": "ka", | |
| 877 | + "name": "Auto·Ka", | |
| 878 | + "domain": "www.auto-ka.com", | |
| 879 | + "node": "M4M64b", | |
| 880 | + "port": 8095, | |
| 881 | + "desc": "Marché automobile : véhicules, prix, fiches v3." | |
| 882 | + }, | |
| 883 | + { | |
| 884 | + "id": "fabri-ka", | |
| 885 | + "group": "ka", | |
| 886 | + "name": "Fabri·Ka", | |
| 887 | + "domain": "www.fabri-ka.com", | |
| 888 | + "node": "M4M64a", | |
| 889 | + "port": 8097, | |
| 890 | + "desc": "Artisans, fabricants et ateliers du Québec." | |
| 891 | + }, | |
| 892 | + { | |
| 893 | + "id": "food-ka", | |
| 894 | + "group": "ka", | |
| 895 | + "name": "Food·Ka", | |
| 896 | + "domain": "www.food-ka.com", | |
| 897 | + "node": "M2U64", | |
| 898 | + "port": 8097, | |
| 899 | + "desc": "Alimentation : produits, commerces, prix." | |
| 900 | + }, | |
| 901 | + { | |
| 902 | + "id": "sorti-ka", | |
| 903 | + "group": "ka", | |
| 904 | + "name": "Sorti·Ka", | |
| 905 | + "domain": "www.sorti-ka.com", | |
| 906 | + "node": "M3U96a", | |
| 907 | + "port": 8120, | |
| 908 | + "desc": "Sorties, événements et activités." | |
| 909 | + }, | |
| 910 | + { | |
| 911 | + "id": "job-ka", | |
| 912 | + "group": "ka", | |
| 913 | + "name": "Job·Ka", | |
| 914 | + "domain": "www.job-ka.com", | |
| 915 | + "node": "M3U96a", | |
| 916 | + "port": 8096, | |
| 917 | + "desc": "Emplois et carrières." | |
| 918 | + }, | |
| 919 | + { | |
| 920 | + "id": "api-ka", | |
| 921 | + "group": "ka", | |
| 922 | + "name": "API·Ka", | |
| 923 | + "domain": "www.api-ka.com", | |
| 924 | + "node": "M4M64b", | |
| 925 | + "port": 8000, | |
| 926 | + "desc": "API unifiée du groupe : Postgres 17, Redis, 24 outils agent." | |
| 927 | + }, | |
| 928 | + { | |
| 929 | + "id": "ka-stats", | |
| 930 | + "group": "ka", | |
| 931 | + "name": "Ka·Stats", | |
| 932 | + "domain": "www.ka-stats.com", | |
| 933 | + "node": "M4M64a", | |
| 934 | + "port": 8140, | |
| 935 | + "desc": "Observatoire statistique des univers Ka." | |
| 936 | + }, | |
| 937 | + { | |
| 938 | + "id": "admin-ka", | |
| 939 | + "group": "ka", | |
| 940 | + "name": "Administration·Ka", | |
| 941 | + "domain": "www.administration-ka.com", | |
| 942 | + "node": "M3U96a", | |
| 943 | + "port": 3300, | |
| 944 | + "desc": "Console d'administration multi-sites, pilotée par le registre mld." | |
| 945 | + }, | |
| 946 | + { | |
| 947 | + "id": "ka2", | |
| 948 | + "group": "ka", | |
| 949 | + "name": "Gardien ka2", | |
| 950 | + "domain": "www.ka2.bot", | |
| 951 | + "node": "M4M36", | |
| 952 | + "port": 8799, | |
| 953 | + "desc": "Agent gardien autonome : surveillance et réparation." | |
| 954 | + }, | |
| 955 | + { | |
| 956 | + "id": "ka4", | |
| 957 | + "group": "ka", | |
| 958 | + "name": "Gardien ka4", | |
| 959 | + "domain": "www.ka4.bot", | |
| 960 | + "node": "M4M36", | |
| 961 | + "port": 8899, | |
| 962 | + "desc": "Agent gardien autonome." | |
| 963 | + }, | |
| 964 | + { | |
| 965 | + "id": "ka6", | |
| 966 | + "group": "ka", | |
| 967 | + "name": "Gardien ka6", | |
| 968 | + "domain": "www.ka6.bot", | |
| 969 | + "node": "M4M36", | |
| 970 | + "port": 8999, | |
| 971 | + "desc": "Agent gardien autonome." | |
| 972 | + }, | |
| 973 | + { | |
| 974 | + "id": "hfmarketdata", | |
| 975 | + "group": "data", | |
| 976 | + "name": "HF Market Data", | |
| 977 | + "domain": "www.hfmarketdata.io", | |
| 978 | + "node": "M3U96b", | |
| 979 | + "port": 8090, | |
| 980 | + "desc": "Données de marché haute fréquence : actions, futures, options, fondamentaux SEC. API + MCP." | |
| 981 | + }, | |
| 982 | + { | |
| 983 | + "id": "rareindex", | |
| 984 | + "group": "data", | |
| 985 | + "name": "RareIndex", | |
| 986 | + "domain": "www.rareindex.io", | |
| 987 | + "node": "M3U96a", | |
| 988 | + "port": 8210, | |
| 989 | + "desc": "Intelligence de marché des objets de collection : 212 connecteurs." | |
| 990 | + }, | |
| 991 | + { | |
| 992 | + "id": "cancerindex", | |
| 993 | + "group": "data", | |
| 994 | + "name": "CancerIndex", | |
| 995 | + "domain": "www.cancerindex.io", | |
| 996 | + "node": "M4M64b", | |
| 997 | + "port": 8250, | |
| 998 | + "desc": "Index mondial des cancers : 9 510 entités NCIt, 13 connecteurs, pgvector." | |
| 999 | + }, | |
| 1000 | + { | |
| 1001 | + "id": "websensor", | |
| 1002 | + "group": "data", | |
| 1003 | + "name": "WebSensor", | |
| 1004 | + "domain": "www.websensor.io", | |
| 1005 | + "node": "M4M64b", | |
| 1006 | + "port": 8260, | |
| 1007 | + "desc": "Détection de changements web : 4 000+ capteurs, 16 connecteurs." | |
| 1008 | + }, | |
| 1009 | + { | |
| 1010 | + "id": "fetcha", | |
| 1011 | + "group": "data", | |
| 1012 | + "name": "Fetcha", | |
| 1013 | + "domain": "www.fetcha.co", | |
| 1014 | + "node": "M3U96a", | |
| 1015 | + "port": 8220, | |
| 1016 | + "desc": "Infrastructure d'accès web intelligent : scrape, crawl, map, SDK." | |
| 1017 | + }, | |
| 1018 | + { | |
| 1019 | + "id": "polyllm", | |
| 1020 | + "group": "data", | |
| 1021 | + "name": "PolyLLM", | |
| 1022 | + "domain": "www.polyllm.io", | |
| 1023 | + "node": "M3U96a", | |
| 1024 | + "port": 8240, | |
| 1025 | + "desc": "Une interface, tous les modèles : 9 fournisseurs, Arena, BYOK." | |
| 1026 | + }, | |
| 1027 | + { | |
| 1028 | + "id": "vquant", | |
| 1029 | + "group": "data", | |
| 1030 | + "name": "vQuant", | |
| 1031 | + "domain": "www.vquant.ai", | |
| 1032 | + "node": "M2U64", | |
| 1033 | + "port": 5000, | |
| 1034 | + "desc": "Terminal quantitatif : indices, portefeuilles, MFA." | |
| 1035 | + }, | |
| 1036 | + { | |
| 1037 | + "id": "qc26", | |
| 1038 | + "group": "data", | |
| 1039 | + "name": "QC26", | |
| 1040 | + "domain": "www.qc26.ca", | |
| 1041 | + "node": "M3U96a", | |
| 1042 | + "port": 8200, | |
| 1043 | + "desc": "L'élection québécoise 2026 en données : sondages, Monte Carlo, revue de presse." | |
| 1044 | + }, | |
| 1045 | + { | |
| 1046 | + "id": "pdb-api", | |
| 1047 | + "group": "data", | |
| 1048 | + "name": "PDB API", | |
| 1049 | + "domain": "www.pdb-api.co", | |
| 1050 | + "node": "M4M64a", | |
| 1051 | + "port": 8270, | |
| 1052 | + "desc": "Explorateur et API de la base SPID (FastAPI + DuckDB)." | |
| 1053 | + }, | |
| 1054 | + { | |
| 1055 | + "id": "uqo-eval", | |
| 1056 | + "group": "uqo", | |
| 1057 | + "name": "UQO Éval", | |
| 1058 | + "domain": "www.uqo-eval.app", | |
| 1059 | + "node": "M2U64", | |
| 1060 | + "port": 8150, | |
| 1061 | + "desc": "Atelier d'évaluation immobilière : hédonique, coût, à vendre, marché." | |
| 1062 | + }, | |
| 1063 | + { | |
| 1064 | + "id": "uqo-imm1003", | |
| 1065 | + "group": "uqo", | |
| 1066 | + "name": "UQO IMM1003", | |
| 1067 | + "domain": "www.uqo-imm1003.app", | |
| 1068 | + "node": "M4M64b", | |
| 1069 | + "port": 8160, | |
| 1070 | + "desc": "Notes de cours interactives et séances vidéo." | |
| 1071 | + }, | |
| 1072 | + { | |
| 1073 | + "id": "uqo-imm1033", | |
| 1074 | + "group": "uqo", | |
| 1075 | + "name": "UQO IMM1033", | |
| 1076 | + "domain": "www.uqo-imm1033.app", | |
| 1077 | + "node": "M3U96a", | |
| 1078 | + "port": 8161, | |
| 1079 | + "desc": "Notes de cours interactives et séances vidéo." | |
| 1080 | + }, | |
| 1081 | + { | |
| 1082 | + "id": "uqo-chat", | |
| 1083 | + "group": "uqo", | |
| 1084 | + "name": "UQO-Chat", | |
| 1085 | + "domain": "www.uqo-chat.app", | |
| 1086 | + "node": "M4M64a", | |
| 1087 | + "port": 8190, | |
| 1088 | + "desc": "Tuteur IA des cours IMM1003 et IMM1033, sur invitation." | |
| 1089 | + }, | |
| 1090 | + { | |
| 1091 | + "id": "maclustr-www", | |
| 1092 | + "group": "platform", | |
| 1093 | + "name": "www.maclustr.io", | |
| 1094 | + "domain": "www.maclustr.io", | |
| 1095 | + "node": "M4M64a", | |
| 1096 | + "port": 8280, | |
| 1097 | + "desc": "Cette page : vitrine du cluster, inventaire et état live des nœuds et des applications." | |
| 1098 | + }, | |
| 1099 | + { | |
| 1100 | + "id": "maclustr-agentd", | |
| 1101 | + "group": "platform", | |
| 1102 | + "name": "maclustr-agentd", | |
| 1103 | + "domain": null, | |
| 1104 | + "node": "M4M64a", | |
| 1105 | + "port": 9210, | |
| 1106 | + "desc": "Agent de supervision : métriques des 33 nœuds, santé des apps, état du tunnel. Alimente les apps macOS et iOS MacLustr et ce site." | |
| 1107 | + }, | |
| 1108 | + { | |
| 1109 | + "id": "maclustr-console", | |
| 1110 | + "group": "platform", | |
| 1111 | + "name": "Console MacLustr", | |
| 1112 | + "domain": "console.maclustr.io", | |
| 1113 | + "node": "M2U64", | |
| 1114 | + "port": 3000, | |
| 1115 | + "desc": "Console web du cluster." | |
| 1116 | + }, | |
| 1117 | + { | |
| 1118 | + "id": "maclustr-appmanager", | |
| 1119 | + "group": "platform", | |
| 1120 | + "name": "App Manager", | |
| 1121 | + "domain": "app.maclustr.io", | |
| 1122 | + "node": "M2U64", | |
| 1123 | + "port": 8787, | |
| 1124 | + "desc": "Gestionnaire d'applications." | |
| 1125 | + }, | |
| 1126 | + { | |
| 1127 | + "id": "maclustr-nodepages", | |
| 1128 | + "group": "platform", | |
| 1129 | + "name": "Node Pages", | |
| 1130 | + "domain": "node.maclustr.io", | |
| 1131 | + "node": "M2U64", | |
| 1132 | + "port": 8003, | |
| 1133 | + "desc": "Pages de bord par nœud." | |
| 1134 | + }, | |
| 1135 | + { | |
| 1136 | + "id": "spbgit", | |
| 1137 | + "group": "platform", | |
| 1138 | + "name": "spbgit", | |
| 1139 | + "domain": "git.spboucher.ai", | |
| 1140 | + "node": "M1M32", | |
| 1141 | + "port": 7420, | |
| 1142 | + "desc": "Forge git privée : dépôts bare de toutes les apps, source de vérité du code." | |
| 1143 | + }, | |
| 1144 | + { | |
| 1145 | + "id": "spboucher-ai", | |
| 1146 | + "group": "spb", | |
| 1147 | + "name": "spboucher.ai", | |
| 1148 | + "domain": "www.spboucher.ai", | |
| 1149 | + "node": "M2U64", | |
| 1150 | + "port": 3100, | |
| 1151 | + "desc": "Site personnel." | |
| 1152 | + }, | |
| 1153 | + { | |
| 1154 | + "id": "spb-cloud", | |
| 1155 | + "group": "spb", | |
| 1156 | + "name": "SPB Cloud", | |
| 1157 | + "domain": "cloud.spboucher.ai", | |
| 1158 | + "node": "M2U64", | |
| 1159 | + "port": 5001, | |
| 1160 | + "desc": "Nuage privé : fichiers, partage, OTP, réorganisation IA." | |
| 1161 | + } | |
| 1162 | + ], | |
| 1163 | + "tunnel": { | |
| 1164 | + "gateways": [ | |
| 1165 | + { | |
| 1166 | + "id": "BHS64", | |
| 1167 | + "ip": "51.161.112.61", | |
| 1168 | + "site": "Beauharnois, Québec", | |
| 1169 | + "primary": true, | |
| 1170 | + "wg": "10.67.0.0/24" | |
| 1171 | + }, | |
| 1172 | + { | |
| 1173 | + "id": "R9128", | |
| 1174 | + "ip": "51.255.75.61", | |
| 1175 | + "site": "Gravelines, France", | |
| 1176 | + "primary": false, | |
| 1177 | + "wg": "10.66.0.0/24" | |
| 1178 | + } | |
| 1179 | + ] | |
| 1180 | + } | |
| 1181 | +} | |
| \ No newline at end of file | ||
added
dossier/dossier.css
+145 −0
@@ -0,0 +1,145 @@ | ||
| 1 | +/* Dossier technique MacLustr — feuille d'impression (A4 paysage) */ | |
| 2 | +@page { size: A4 landscape; margin: 13mm 14mm 15mm 14mm; } | |
| 3 | +:root { | |
| 4 | + --ink: #0f1115; --ink-2: #4b5060; --ink-3: #868b99; --line: #dfe2ea; --line-2: #c9cdd8; | |
| 5 | + --paper: #ffffff; --paper-2: #f5f6f9; --paper-3: #eceef4; | |
| 6 | + --accent: #2563eb; --accent-2: #1d4ed8; --accent-dim: #e8effd; --alt: #7c3aed; | |
| 7 | + --good: #16a34a; --bad: #dc2626; --off: #9aa0ae; | |
| 8 | + --sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", Inter, Arial, sans-serif; | |
| 9 | + --mono: "SF Mono", ui-monospace, Menlo, Consolas, monospace; | |
| 10 | +} | |
| 11 | +* { box-sizing: border-box; } | |
| 12 | +html, body { margin: 0; padding: 0; } | |
| 13 | +body { font: 9pt/1.42 var(--sans); color: var(--ink); background: var(--paper); -webkit-print-color-adjust: exact; print-color-adjust: exact; } | |
| 14 | +h1, h2, h3, h4 { margin: 0; letter-spacing: -.02em; line-height: 1.12; } | |
| 15 | +p { margin: 0 0 6px; } | |
| 16 | +code { font: 8.4pt var(--mono); background: var(--paper-2); border: 1px solid var(--line); border-radius: 3px; padding: 0 3px; color: var(--accent-2); } | |
| 17 | +strong { font-weight: 650; } | |
| 18 | +.mono { font-family: var(--mono); } | |
| 19 | +.small { font-size: 8pt; } | |
| 20 | +.faint { color: var(--ink-3); } | |
| 21 | +.r { text-align: right; } | |
| 22 | +.nw { white-space: nowrap; } | |
| 23 | +.logo { width: 22px; height: 22px; color: var(--accent); vertical-align: middle; } | |
| 24 | +.brand { display: inline-flex; align-items: center; gap: 8px; font-weight: 700; font-size: 13pt; letter-spacing: -.02em; } | |
| 25 | +.eyebrow { font-size: 7.6pt; text-transform: uppercase; letter-spacing: .14em; color: var(--ink-3); font-weight: 600; margin: 0 0 8px; } | |
| 26 | + | |
| 27 | +/* ---------- couverture ---------- */ | |
| 28 | +body.cover { background: #09090b; color: #f4f4f5; width: 297mm; height: 210mm; overflow: hidden; } | |
| 29 | +.cover .kpi { background: #111114; border: 0; border-radius: 0; } | |
| 30 | +.cover .kpi b { color: #fff; } .cover .kpi span { color: #a1a1aa; } .cover .kpi small { color: #6b6b76; } | |
| 31 | +.cv { position: relative; width: 297mm; height: 210mm; overflow: hidden; padding: 14mm 18mm; display: flex; flex-direction: column; justify-content: space-between; background: radial-gradient(70% 55% at 80% 0%, rgba(96,165,250,.16), transparent 70%), #09090b; } | |
| 32 | +.cv::after { content: ""; position: absolute; inset: 0; background-image: linear-gradient(rgba(255,255,255,.035) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,.035) 1px, transparent 1px); background-size: 14mm 14mm; -webkit-mask-image: radial-gradient(ellipse at 50% 0%, #000 0%, transparent 75%); mask-image: radial-gradient(ellipse at 50% 0%, #000 0%, transparent 75%); pointer-events: none; } | |
| 33 | +.cv > * { position: relative; z-index: 1; } | |
| 34 | +.cv .brand { color: #f4f4f5; } | |
| 35 | +.cv .logo { color: #60a5fa; } | |
| 36 | +.cv-top { display: flex; justify-content: space-between; align-items: center; color: #a1a1aa; font-size: 9pt; } | |
| 37 | +.cv-mid { max-width: 190mm; } | |
| 38 | +.cv .eyebrow { color: #a1a1aa; } | |
| 39 | +.cv h1 { font-size: 52pt; font-weight: 750; letter-spacing: -.04em; line-height: .96; color: #fff; } | |
| 40 | +.cv h1 span { color: #6b6b76; } | |
| 41 | +.cv .sub { margin-top: 7mm; font-size: 11.5pt; line-height: 1.5; color: #c9cbd3; max-width: 160mm; } | |
| 42 | +.cv-facts { display: grid; grid-template-columns: repeat(6, 1fr); border: 1px solid #26262d; border-radius: 10px; overflow: hidden; background: #111114; } | |
| 43 | +.cv-facts .kpi { padding: 5mm 5mm; border-right: 1px solid #26262d; } | |
| 44 | +.cv-facts .kpi:last-child { border-right: 0; } | |
| 45 | +.cv-facts .kpi b { display: block; font: 600 22pt/1.05 var(--mono); letter-spacing: -.03em; color: #fff; } | |
| 46 | +.cv-facts .kpi span { display: block; color: #a1a1aa; font-size: 8.6pt; margin-top: 2px; } | |
| 47 | +.cv-facts .kpi small { display: block; color: #6b6b76; font: 7.6pt var(--mono); margin-top: 4px; } | |
| 48 | +.cv-bot { display: flex; justify-content: space-between; color: #6b6b76; font-size: 8.4pt; } | |
| 49 | + | |
| 50 | +/* ---------- corps ---------- */ | |
| 51 | +.sec { break-before: page; } | |
| 52 | +.sec:first-child { break-before: auto; } | |
| 53 | +.sec-h { display: flex; align-items: baseline; gap: 12px; border-bottom: 2px solid var(--ink); padding-bottom: 6px; margin-bottom: 10px; } | |
| 54 | +.sec-h .num { font: 600 11pt var(--mono); color: var(--accent); } | |
| 55 | +.sec-h h2 { font-size: 20pt; font-weight: 700; } | |
| 56 | +h3 { font-size: 12pt; font-weight: 650; margin: 10px 0 5px; break-after: avoid; display: flex; align-items: baseline; gap: 8px; } | |
| 57 | +h3 .cnt { font: 500 8.5pt var(--mono); color: var(--ink-3); } | |
| 58 | +h4 { font-size: 9.5pt; font-weight: 650; color: var(--ink-2); text-transform: uppercase; letter-spacing: .08em; margin-bottom: 8px; } | |
| 59 | +.lead { color: var(--ink-2); font-size: 9.1pt; max-width: 245mm; margin-bottom: 4px; } | |
| 60 | +.lead.big { font-size: 12pt; line-height: 1.5; color: var(--ink); margin-bottom: 10px; } | |
| 61 | +.note { color: var(--ink-3); font-size: 8.4pt; margin-top: 4px; } | |
| 62 | +.two { display: grid; grid-template-columns: 1fr 1fr; gap: 10mm; align-items: start; } | |
| 63 | +.three-col { display: grid; grid-template-columns: 1.1fr 1fr 1.2fr; gap: 5mm; align-items: start; } | |
| 64 | +.kpis { display: grid; grid-template-columns: repeat(6, 1fr); gap: 4mm; margin: 6px 0 4px; } | |
| 65 | +.kpis.three { grid-template-columns: repeat(3, 1fr); } | |
| 66 | +.kpi { border: 1px solid var(--line); border-radius: 8px; padding: 7px 10px; background: var(--paper); break-inside: avoid; } | |
| 67 | +.kpi b { display: block; font: 600 17pt/1.05 var(--mono); letter-spacing: -.03em; } | |
| 68 | +.kpi span { display: block; color: var(--ink-2); font-size: 8.4pt; margin-top: 2px; } | |
| 69 | +.kpi small { display: block; color: var(--ink-3); font: 7.4pt var(--mono); margin-top: 4px; } | |
| 70 | +.toc { border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; background: var(--paper-2); } | |
| 71 | +.toc div { display: flex; gap: 10px; padding: 3px 0; border-bottom: 1px dashed var(--line); font-size: 9pt; } | |
| 72 | +.toc div:last-child { border-bottom: 0; } | |
| 73 | +.toc .num { font: 600 8.4pt var(--mono); color: var(--accent); width: 18px; } | |
| 74 | + | |
| 75 | +/* tables */ | |
| 76 | +table.t { width: 100%; border-collapse: collapse; margin: 4px 0 10px; font-size: 8.2pt; break-inside: auto; } | |
| 77 | +table.t th { text-align: left; font-size: 7.4pt; text-transform: uppercase; letter-spacing: .06em; color: var(--ink-3); font-weight: 600; padding: 5px 6px; border-bottom: 1.5px solid var(--ink); white-space: nowrap; } | |
| 78 | +table.t th.r { text-align: right; } | |
| 79 | +table.t td { padding: 4px 6px; border-bottom: 1px solid var(--line); vertical-align: top; overflow-wrap: anywhere; } | |
| 80 | +table.t tr { break-inside: avoid; } | |
| 81 | +table.t thead { display: table-header-group; } | |
| 82 | +table.t td.id { font: 600 8.4pt var(--mono); white-space: nowrap; color: var(--ink); } | |
| 83 | +table.t td.mono, .mono { font-family: var(--mono); } | |
| 84 | +table.t td code { font-size: 7.6pt; } | |
| 85 | +table.t.hw { font-size: 7.5pt; } | |
| 86 | +table.t.hw td { padding: 3px 4px; line-height: 1.3; } | |
| 87 | +table.t.over td:first-child, table.t.models td:first-child { font-weight: 500; } | |
| 88 | +table.t.roles td { font-size: 7.4pt; padding: 3px 5px; line-height: 1.3; } | |
| 89 | +.roles-page { break-before: page; } | |
| 90 | +.keep { break-inside: avoid; } | |
| 91 | +table.t.ovh td { font-size: 7.4pt; line-height: 1.3; } | |
| 92 | +table.t.ovh td:nth-child(6) { min-width: 34mm; } table.t.ovh td code { white-space: normal; word-break: break-all; } | |
| 93 | +.kpis.stack { grid-template-columns: 1fr; gap: 3mm; } | |
| 94 | +table.t.tun td:first-child { font: 600 8pt var(--mono); } | |
| 95 | +table.t.apps td { font-size: 7.8pt; padding: 3px 6px; } | |
| 96 | +.app-group { margin-bottom: 4px; } | |
| 97 | +table.t.heat td, table.t.bars td { vertical-align: middle; } | |
| 98 | +table.t.bars td { padding: 3px 6px; } | |
| 99 | +table.t.bars td small { line-height: 1.1; } | |
| 100 | +td small { color: var(--ink-3); font-size: 7.4pt; } | |
| 101 | + | |
| 102 | +/* barres */ | |
| 103 | +.meter { white-space: nowrap; } | |
| 104 | +.bar { display: inline-block; width: 52%; height: 6px; border-radius: 3px; background: var(--paper-3); vertical-align: middle; overflow: hidden; margin-right: 6px; } | |
| 105 | +.bar i { display: block; height: 100%; background: var(--accent); border-radius: 3px; } | |
| 106 | +.bar.alt i { background: var(--alt); } | |
| 107 | +.meter.wide .bar { width: 58%; } | |
| 108 | +.meter .v { font: 7.8pt var(--mono); color: var(--ink-2); vertical-align: middle; } | |
| 109 | + | |
| 110 | +/* statuts */ | |
| 111 | +.st { display: inline-block; padding: 1px 7px; border-radius: 999px; font-size: 7.2pt; font-weight: 600; border: 1px solid var(--line-2); color: var(--ink-2); white-space: nowrap; } | |
| 112 | +.st::before { content: ""; display: inline-block; width: 5px; height: 5px; border-radius: 50%; background: var(--off); margin-right: 5px; vertical-align: middle; } | |
| 113 | +.st.on { color: #166534; border-color: #bbf7d0; background: #f0fdf4; } .st.on::before { background: var(--good); } | |
| 114 | +.st.bad { color: #991b1b; border-color: #fecaca; background: #fef2f2; } .st.bad::before { background: var(--bad); } | |
| 115 | +.st.off { color: var(--ink-3); background: var(--paper-2); } | |
| 116 | + | |
| 117 | +/* fiches, panneaux */ | |
| 118 | +.fiches p { padding: 6px 9px; border-left: 3px solid var(--accent); background: var(--paper-2); border-radius: 0 6px 6px 0; margin-bottom: 5px; font-size: 8pt; break-inside: avoid; } | |
| 119 | +.panel { border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; margin-bottom: 8px; break-inside: avoid; } | |
| 120 | +.flow { margin: 0; padding: 0; list-style: none; counter-reset: f; } | |
| 121 | +.flow li { display: grid; grid-template-columns: 20px 1fr; gap: 8px; align-items: start; padding: 5px 0; border-bottom: 1px dashed var(--line); font-size: 8.6pt; } | |
| 122 | +.flow li:last-child { border-bottom: 0; } | |
| 123 | +.flow li::before { counter-increment: f; content: counter(f, decimal-leading-zero); font: 600 8pt var(--mono); color: var(--accent); } | |
| 124 | +.flow li b { display: block; font-weight: 650; } | |
| 125 | +.flow li div span { color: var(--ink-2); display: block; } | |
| 126 | +.gw { display: grid; grid-template-columns: 70px 1fr; gap: 2px 10px; padding: 6px 0; border-bottom: 1px dashed var(--line); font-size: 8.6pt; } | |
| 127 | +.gw:last-child { border-bottom: 0; } | |
| 128 | +.gw b { font: 600 9pt var(--mono); grid-row: span 2; align-self: center; } | |
| 129 | +.gw span { color: var(--ink-2); } | |
| 130 | +.gw .mono { font-size: 7.8pt; color: var(--ink-3); } | |
| 131 | + | |
| 132 | +/* carte et sites */ | |
| 133 | +figure.map { margin: 0 0 8px; break-inside: avoid; } | |
| 134 | +figure.map img { width: 100%; height: 78mm; object-fit: cover; object-position: center 40%; border-radius: 8px; border: 1px solid var(--line); display: block; } | |
| 135 | +figure.map figcaption { font-size: 7.8pt; color: var(--ink-3); margin-top: 4px; } | |
| 136 | +.sites { display: grid; grid-template-columns: repeat(4, 1fr); gap: 3mm; margin-top: 6px; } | |
| 137 | +.site { border: 1px solid var(--line); border-radius: 8px; padding: 7px 9px; break-inside: avoid; } | |
| 138 | +.site .s-h { display: flex; justify-content: space-between; align-items: baseline; gap: 6px; font-size: 9.8pt; } | |
| 139 | +.site .s-h .mono { font-size: 8pt; color: var(--ink-2); } | |
| 140 | +.site .s-r { color: var(--ink-3); font-size: 7.8pt; margin: 2px 0 6px; } | |
| 141 | +.site p { font-size: 7.5pt; color: var(--ink-2); min-height: 20px; margin-bottom: 3px; } | |
| 142 | +.site .s-n { display: flex; flex-wrap: wrap; gap: 3px; } | |
| 143 | +.site .s-n span { font: 7.4pt var(--mono); padding: 1px 5px; border-radius: 4px; background: var(--paper-2); border: 1px solid var(--line); } | |
| 144 | + | |
| 145 | +.end { display: flex; align-items: center; gap: 8px; margin-top: 18px; color: var(--ink-3); font: 8.4pt var(--mono); } | |
added
dossier/live.json
+1 −0
@@ -0,0 +1 @@ | ||
| 1 | +{"ts":1789095615879,"agentVersion":"2.1.0","summary":{"nodesTotal":33,"nodesOnline":32,"apps":{"total":37,"up":36,"degraded":1,"down":0,"unknown":0,"cycle":605},"coresOnline":398,"gpuOnline":802,"ramOnlineGB":1008,"ramUsedGB":296,"cpuAvg":13.6,"diskUsedGB":362,"diskTotalGB":18458,"netInKBs":620,"netOutKBs":271,"procs":20870,"tunnelRx":51815190192,"tunnelTx":1832434372},"nodes":{"M3U96a":{"online":true,"cpu":15.2,"gpu":0,"load1":3.11,"memUsedGB":17.1,"memTotalGB":96,"swapGB":0,"diskUsedGB":11,"diskTotalGB":926,"netInKBs":85,"netOutKBs":159,"procs":645,"uptimeS":25301,"os":"26.6","thermal":"nominal","apps":{"total":9,"up":9,"degraded":0,"down":0,"unknown":0,"apps":["vrai-prix","sorti-ka","job-ka","admin-ka","uqo-imm1033","qc26","rareindex","fetcha","polyllm"]}},"M3U96b":{"online":true,"cpu":10.3,"gpu":2,"load1":1.91,"memUsedGB":15.9,"memTotalGB":96,"swapGB":0,"diskUsedGB":11,"diskTotalGB":926,"netInKBs":1,"netOutKBs":1,"procs":620,"uptimeS":24878,"os":"26.4","thermal":"nominal","apps":{"total":1,"up":1,"degraded":0,"down":0,"unknown":0,"apps":["hfmarketdata"]}},"M2U64":{"online":true,"cpu":5.4,"gpu":1,"load1":1.16,"memUsedGB":11.1,"memTotalGB":64,"swapGB":0,"diskUsedGB":11,"diskTotalGB":926,"netInKBs":11,"netOutKBs":19,"procs":699,"uptimeS":25302,"os":"26.5","thermal":"nominal","apps":{"total":8,"up":8,"degraded":0,"down":0,"unknown":0,"apps":["uqo-eval","food-ka","spb-cloud","vquant","spboucher-ai","maclustr-console","maclustr-appmanager","maclustr-nodepages"]}},"M4M64a":{"online":true,"cpu":25.2,"gpu":0,"load1":1.61,"memUsedGB":13.4,"memTotalGB":64,"swapGB":0,"diskUsedGB":11,"diskTotalGB":926,"netInKBs":14,"netOutKBs":15,"procs":624,"uptimeS":24879,"os":"26.5.1","thermal":"nominal","apps":{"total":7,"up":7,"degraded":0,"down":0,"unknown":0,"apps":["ka-stats","fabri-ka","maclustr-agentd","lou-ka","uqo-chat","pdb-api","maclustr-www"]}},"M4M64b":{"online":true,"cpu":14.9,"gpu":0,"load1":3.06,"memUsedGB":14.1,"memTotalGB":64,"swapGB":0,"diskUsedGB":11,"diskTotalGB":926,"netInKBs":398,"netOutKBs":57,"procs":667,"uptimeS":25303,"os":"26.5.1","thermal":"nominal","apps":{"total":7,"up":7,"degraded":0,"down":0,"unknown":0,"apps":["immo-ka","auto-ka","groupe-ka","api-ka","uqo-imm1003","cancerindex","websensor"]}},"M4BP48":{"online":true,"cpu":7.5,"gpu":5,"load1":1.49,"memUsedGB":12.1,"memTotalGB":48,"swapGB":0,"diskUsedGB":12,"diskTotalGB":926,"netInKBs":1,"netOutKBs":1,"procs":737,"uptimeS":8522753,"os":"26.5.1","thermal":"nominal","apps":null},"M4BP36":{"online":true,"cpu":16.7,"gpu":8,"load1":2.33,"memUsedGB":10,"memTotalGB":36,"swapGB":0,"diskUsedGB":11,"diskTotalGB":926,"netInKBs":1,"netOutKBs":0,"procs":666,"uptimeS":521545,"os":"26.5.1","thermal":"nominal","apps":null},"M4M36":{"online":true,"cpu":9.6,"gpu":0,"load1":1.44,"memUsedGB":8.3,"memTotalGB":36,"swapGB":0,"diskUsedGB":11,"diskTotalGB":460,"netInKBs":6,"netOutKBs":2,"procs":614,"uptimeS":25306,"os":"26.5.2","thermal":"nominal","apps":{"total":3,"up":3,"degraded":0,"down":0,"unknown":0,"apps":["ka2","ka4","ka6"]}},"M2M32":{"online":true,"cpu":10.4,"gpu":0,"load1":1.08,"memUsedGB":7.7,"memTotalGB":32,"swapGB":0,"diskUsedGB":11,"diskTotalGB":460,"netInKBs":1,"netOutKBs":0,"procs":595,"uptimeS":25305,"os":"26.5.1","thermal":"nominal","apps":null},"M2M32b":{"online":true,"cpu":9.9,"gpu":0,"load1":1.2,"memUsedGB":7.4,"memTotalGB":32,"swapGB":0,"diskUsedGB":11,"diskTotalGB":460,"netInKBs":1,"netOutKBs":0,"procs":571,"uptimeS":24867,"os":"26.5.1","thermal":"nominal","apps":null},"M2M32c":{"online":true,"cpu":12.3,"gpu":0,"load1":1.3,"memUsedGB":7.8,"memTotalGB":32,"swapGB":0,"diskUsedGB":11,"diskTotalGB":460,"netInKBs":1,"netOutKBs":0,"procs":606,"uptimeS":25305,"os":"26.5.1","thermal":"nominal","apps":null},"m4mc":{"online":true,"cpu":10.4,"gpu":0,"load1":1.51,"memUsedGB":7.5,"memTotalGB":24,"swapGB":0,"diskUsedGB":11,"diskTotalGB":460,"netInKBs":1,"netOutKBs":0,"procs":560,"uptimeS":24870,"os":"26.5.1","thermal":"nominal","apps":null},"M1M32":{"online":true,"cpu":13.4,"gpu":2,"load1":1.63,"memUsedGB":8,"memTotalGB":32,"swapGB":0,"diskUsedGB":11,"diskTotalGB":460,"netInKBs":11,"netOutKBs":6,"procs":646,"uptimeS":24868,"os":"26.5.1","thermal":"nominal","apps":{"total":1,"up":1,"degraded":0,"down":0,"unknown":0,"apps":["spbgit"]}},"m4ma":{"online":true,"cpu":12.5,"gpu":0,"load1":1.45,"memUsedGB":7.3,"memTotalGB":24,"swapGB":0,"diskUsedGB":11,"diskTotalGB":460,"netInKBs":1,"netOutKBs":0,"procs":574,"uptimeS":24883,"os":"26.5.1","thermal":"nominal","apps":null},"m4mb":{"online":true,"cpu":13.3,"gpu":0,"load1":2.14,"memUsedGB":7.4,"memTotalGB":16,"swapGB":0,"diskUsedGB":11,"diskTotalGB":228,"netInKBs":1,"netOutKBs":0,"procs":585,"uptimeS":24870,"os":"26.5.1","thermal":"nominal","apps":null},"m4md":{"online":false,"cpu":null,"gpu":null,"load1":null,"memUsedGB":null,"memTotalGB":null,"swapGB":null,"diskUsedGB":null,"diskTotalGB":null,"netInKBs":null,"netOutKBs":null,"procs":null,"uptimeS":null,"os":null,"thermal":null,"apps":null},"m2m16":{"online":true,"cpu":20.5,"gpu":0,"load1":1.22,"memUsedGB":7.5,"memTotalGB":16,"swapGB":0,"diskUsedGB":11,"diskTotalGB":460,"netInKBs":1,"netOutKBs":0,"procs":588,"uptimeS":25305,"os":"26.4.1","thermal":"nominal","apps":null},"M3BA24":{"online":true,"cpu":14.5,"gpu":11,"load1":1.89,"memUsedGB":10.4,"memTotalGB":24,"swapGB":0,"diskUsedGB":11,"diskTotalGB":460,"netInKBs":1,"netOutKBs":0,"procs":670,"uptimeS":539732,"os":"26.5.1","thermal":"nominal","apps":null},"M3BA16":{"online":true,"cpu":17,"gpu":11,"load1":1.36,"memUsedGB":9.3,"memTotalGB":16,"swapGB":0,"diskUsedGB":12,"diskTotalGB":460,"netInKBs":1,"netOutKBs":1,"procs":806,"uptimeS":8565244,"os":"26.5.1","thermal":"nominal","apps":null},"m2m8a":{"online":true,"cpu":18.5,"gpu":3,"load1":1.4,"memUsedGB":4.7,"memTotalGB":8,"swapGB":0,"diskUsedGB":11,"diskTotalGB":460,"netInKBs":1,"netOutKBs":0,"procs":627,"uptimeS":25308,"os":"26.5.1","thermal":"nominal","apps":null},"m2m8b":{"online":true,"cpu":12.5,"gpu":0,"load1":1.3,"memUsedGB":4.7,"memTotalGB":8,"swapGB":0,"diskUsedGB":11,"diskTotalGB":228,"netInKBs":1,"netOutKBs":0,"procs":601,"uptimeS":24919,"os":"26.5.1","thermal":"nominal","apps":null},"M1M64":{"online":true,"cpu":12.2,"gpu":0,"load1":1.54,"memUsedGB":8.3,"memTotalGB":64,"swapGB":0,"diskUsedGB":11,"diskTotalGB":1858,"netInKBs":34,"netOutKBs":4,"procs":791,"uptimeS":239831,"os":"26.6.2","thermal":"nominal","apps":{"total":1,"up":0,"degraded":1,"down":0,"unknown":0,"apps":["llm-api"]}},"m2m16b":{"online":true,"cpu":25.9,"gpu":8,"load1":2.36,"memUsedGB":8.3,"memTotalGB":16,"swapGB":0,"diskUsedGB":15,"diskTotalGB":926,"netInKBs":11,"netOutKBs":0,"procs":335,"uptimeS":310169,"os":"26.6","thermal":"nominal","apps":null},"m1m16":{"online":true,"cpu":12,"gpu":0,"load1":1.07,"memUsedGB":8.6,"memTotalGB":16,"swapGB":0,"diskUsedGB":11,"diskTotalGB":926,"netInKBs":3,"netOutKBs":1,"procs":652,"uptimeS":545396,"os":"26.6.2","thermal":"nominal","apps":null},"m2m16c":{"online":true,"cpu":13.3,"gpu":0,"load1":1.54,"memUsedGB":8.6,"memTotalGB":16,"swapGB":0,"diskUsedGB":11,"diskTotalGB":926,"netInKBs":3,"netOutKBs":1,"procs":641,"uptimeS":320549,"os":"26.6.2","thermal":"nominal","apps":null},"m4me":{"online":true,"cpu":14,"gpu":3,"load1":1.5,"memUsedGB":8.9,"memTotalGB":16,"swapGB":0,"diskUsedGB":11,"diskTotalGB":228,"netInKBs":1,"netOutKBs":0,"procs":739,"uptimeS":196476,"os":"26.6.2","thermal":"nominal","apps":null},"m4mf":{"online":true,"cpu":11.5,"gpu":3,"load1":1.66,"memUsedGB":8.7,"memTotalGB":16,"swapGB":0,"diskUsedGB":11,"diskTotalGB":228,"netInKBs":2,"netOutKBs":1,"procs":757,"uptimeS":112872,"os":"26.6.2","thermal":"nominal","apps":null},"m4mg":{"online":true,"cpu":10,"gpu":11,"load1":1.41,"memUsedGB":8.6,"memTotalGB":16,"swapGB":0,"diskUsedGB":11,"diskTotalGB":228,"netInKBs":8,"netOutKBs":1,"procs":707,"uptimeS":112298,"os":"26.6.2","thermal":"nominal","apps":null},"m4mh":{"online":true,"cpu":12.6,"gpu":3,"load1":1.63,"memUsedGB":9.1,"memTotalGB":16,"swapGB":0,"diskUsedGB":11,"diskTotalGB":228,"netInKBs":1,"netOutKBs":1,"procs":710,"uptimeS":67406,"os":"26.6.2","thermal":"nominal","apps":null},"m1m16b":{"online":true,"cpu":16,"gpu":3,"load1":1.27,"memUsedGB":9,"memTotalGB":16,"swapGB":0,"diskUsedGB":15,"diskTotalGB":228,"netInKBs":1,"netOutKBs":0,"procs":750,"uptimeS":70095,"os":"26.6.1","thermal":"nominal","apps":null},"m4mi":{"online":true,"cpu":14.7,"gpu":3,"load1":1.57,"memUsedGB":8.8,"memTotalGB":16,"swapGB":0,"diskUsedGB":11,"diskTotalGB":228,"netInKBs":16,"netOutKBs":1,"procs":702,"uptimeS":57979,"os":"26.6.2","thermal":"nominal","apps":null},"m4mj":{"online":true,"cpu":11.8,"gpu":3,"load1":1.17,"memUsedGB":8.8,"memTotalGB":16,"swapGB":0,"diskUsedGB":11,"diskTotalGB":228,"netInKBs":1,"netOutKBs":0,"procs":696,"uptimeS":58425,"os":"26.6.2","thermal":"nominal","apps":null},"m4mk":{"online":true,"cpu":11,"gpu":3,"load1":1.42,"memUsedGB":8.6,"memTotalGB":16,"swapGB":0,"diskUsedGB":11,"diskTotalGB":228,"netInKBs":0,"netOutKBs":0,"procs":689,"uptimeS":44622,"os":"26.6.2","thermal":"nominal","apps":null}},"apps":{"llm-api":{"node":"M1M64","state":"degraded","publicOk":false,"publicMs":5035,"localOk":true,"localMs":2,"uptime24h":92.31,"via":"BHS64","memMB":237.4,"cpu":5},"admin-ka":{"node":"M3U96a","state":"up","publicOk":true,"publicMs":44,"localOk":true,"localMs":0,"uptime24h":89.58,"via":"BHS64","memMB":0,"cpu":0},"api-ka":{"node":"M4M64b","state":"up","publicOk":true,"publicMs":79,"localOk":true,"localMs":7,"uptime24h":93.96,"via":"BHS64","memMB":309.7,"cpu":0.3},"auto-ka":{"node":"M4M64b","state":"up","publicOk":true,"publicMs":300,"localOk":true,"localMs":199,"uptime24h":93.96,"via":"BHS64","memMB":540.3,"cpu":4.6},"cancerindex":{"node":"M4M64b","state":"up","publicOk":true,"publicMs":49,"localOk":true,"localMs":1,"uptime24h":93.96,"via":"BHS64","memMB":260.9,"cpu":0.1},"fabri-ka":{"node":"M4M64a","state":"up","publicOk":true,"publicMs":39,"localOk":true,"localMs":1,"uptime24h":99.92,"via":"BHS64","memMB":807.2,"cpu":0.5},"fetcha":{"node":"M3U96a","state":"up","publicOk":true,"publicMs":48,"localOk":true,"localMs":1,"uptime24h":89.58,"via":"BHS64","memMB":241.9,"cpu":0.1},"food-ka":{"node":"M2U64","state":"up","publicOk":true,"publicMs":134,"localOk":true,"localMs":60,"uptime24h":92.99,"via":"BHS64","memMB":403.2,"cpu":1.1},"groupe-ka":{"node":"M4M64b","state":"up","publicOk":true,"publicMs":77,"localOk":true,"localMs":4,"uptime24h":93.96,"via":"BHS64","memMB":297.8,"cpu":0.2},"hfmarketdata":{"node":"M3U96b","state":"up","publicOk":true,"publicMs":79,"localOk":true,"localMs":5,"uptime24h":93.65,"via":"BHS64","memMB":397.3,"cpu":0},"immo-ka":{"node":"M4M64b","state":"up","publicOk":true,"publicMs":66,"localOk":true,"localMs":2,"uptime24h":93.96,"via":"BHS64","memMB":539.5,"cpu":0.4},"job-ka":{"node":"M3U96a","state":"up","publicOk":true,"publicMs":146,"localOk":true,"localMs":89,"uptime24h":89.58,"via":"BHS64","memMB":638.7,"cpu":1.8},"ka-stats":{"node":"M4M64a","state":"up","publicOk":true,"publicMs":47,"localOk":true,"localMs":0,"uptime24h":99.96,"via":"BHS64","memMB":109.3,"cpu":0},"ka2":{"node":"M4M36","state":"up","publicOk":true,"publicMs":74,"localOk":true,"localMs":2,"uptime24h":93.96,"via":"BHS64","memMB":0,"cpu":0},"ka4":{"node":"M4M36","state":"up","publicOk":true,"publicMs":73,"localOk":true,"localMs":2,"uptime24h":93.96,"via":"BHS64","memMB":0,"cpu":0},"ka6":{"node":"M4M36","state":"up","publicOk":true,"publicMs":76,"localOk":true,"localMs":2,"uptime24h":93.96,"via":"BHS64","memMB":0,"cpu":0},"lou-ka":{"node":"M4M64a","state":"up","publicOk":true,"publicMs":350,"localOk":true,"localMs":269,"uptime24h":99.96,"via":"BHS64","memMB":844.5,"cpu":2.5},"maclustr-agentd":{"node":"M4M64a","state":"up","publicOk":null,"publicMs":null,"localOk":true,"localMs":0,"uptime24h":99.96,"via":null,"memMB":0,"cpu":0},"maclustr-appmanager":{"node":"M2U64","state":"up","publicOk":true,"publicMs":78,"localOk":true,"localMs":0,"uptime24h":92.99,"via":"BHS64","memMB":77,"cpu":0.9},"maclustr-console":{"node":"M2U64","state":"up","publicOk":true,"publicMs":74,"localOk":true,"localMs":1,"uptime24h":92.99,"via":"BHS64","memMB":74,"cpu":0.1},"maclustr-nodepages":{"node":"M2U64","state":"up","publicOk":true,"publicMs":68,"localOk":true,"localMs":0,"uptime24h":92.99,"via":"BHS64","memMB":19.8,"cpu":0},"maclustr-www":{"node":"M4M64a","state":"up","publicOk":true,"publicMs":46,"localOk":true,"localMs":0,"uptime24h":98.59,"via":"BHS64","memMB":89.1,"cpu":0},"pdb-api":{"node":"M4M64a","state":"up","publicOk":true,"publicMs":40,"localOk":true,"localMs":1,"uptime24h":99.96,"via":"BHS64","memMB":592,"cpu":0.1},"polyllm":{"node":"M3U96a","state":"up","publicOk":true,"publicMs":46,"localOk":true,"localMs":1,"uptime24h":89.58,"via":"BHS64","memMB":161.4,"cpu":0},"qc26":{"node":"M3U96a","state":"up","publicOk":true,"publicMs":58,"localOk":true,"localMs":1,"uptime24h":89.12,"via":"BHS64","memMB":687.3,"cpu":0.1},"rareindex":{"node":"M3U96a","state":"up","publicOk":true,"publicMs":49,"localOk":true,"localMs":1,"uptime24h":89.58,"via":"BHS64","memMB":489.5,"cpu":0},"sorti-ka":{"node":"M3U96a","state":"up","publicOk":true,"publicMs":158,"localOk":true,"localMs":86,"uptime24h":89.5,"via":"BHS64","memMB":463.9,"cpu":1.1},"spb-cloud":{"node":"M2U64","state":"up","publicOk":true,"publicMs":76,"localOk":true,"localMs":1,"uptime24h":92.99,"via":"BHS64","memMB":73.5,"cpu":0},"spbgit":{"node":"M1M32","state":"up","publicOk":true,"publicMs":94,"localOk":true,"localMs":64,"uptime24h":93.88,"via":"BHS64","memMB":447,"cpu":1.5},"spboucher-ai":{"node":"M2U64","state":"up","publicOk":true,"publicMs":76,"localOk":true,"localMs":2,"uptime24h":92.99,"via":"BHS64","memMB":76.4,"cpu":0},"uqo-chat":{"node":"M4M64a","state":"up","publicOk":true,"publicMs":52,"localOk":true,"localMs":1,"uptime24h":99.96,"via":"BHS64","memMB":180.3,"cpu":0.3},"uqo-eval":{"node":"M2U64","state":"up","publicOk":true,"publicMs":47,"localOk":true,"localMs":3,"uptime24h":92.95,"via":"BHS64","memMB":68.1,"cpu":0},"uqo-imm1003":{"node":"M4M64b","state":"up","publicOk":true,"publicMs":48,"localOk":true,"localMs":0,"uptime24h":93.96,"via":"BHS64","memMB":74.5,"cpu":0},"uqo-imm1033":{"node":"M3U96a","state":"up","publicOk":true,"publicMs":61,"localOk":true,"localMs":0,"uptime24h":89.58,"via":"BHS64","memMB":91.4,"cpu":0},"vquant":{"node":"M2U64","state":"up","publicOk":true,"publicMs":79,"localOk":true,"localMs":3,"uptime24h":92.99,"via":"BHS64","memMB":60,"cpu":0},"vrai-prix":{"node":"M3U96a","state":"up","publicOk":true,"publicMs":60,"localOk":true,"localMs":1,"uptime24h":89.54,"via":"BHS64","memMB":438.9,"cpu":15.4},"websensor":{"node":"M4M64b","state":"up","publicOk":true,"publicMs":42,"localOk":true,"localMs":1,"uptime24h":93.96,"via":"BHS64","memMB":276,"cpu":0}},"tunnel":[{"name":"BHS64","ok":true,"primary":true,"site":"Beauharnois (Québec)","peers":[{"alias":"M1M32","online":true,"handshakeS":52,"rx":729781756,"tx":34626052},{"alias":"M3U96a","online":true,"handshakeS":69,"rx":9305282796,"tx":998475428},{"alias":"M4M64b","online":true,"handshakeS":83,"rx":1796881200,"tx":118095608},{"alias":"M3U96b","online":true,"handshakeS":52,"rx":36906857384,"tx":310778300},{"alias":"M2U64","online":true,"handshakeS":84,"rx":1758892408,"tx":199096248},{"alias":"M4M64a","online":true,"handshakeS":114,"rx":808626848,"tx":144390892},{"alias":"M4M36","online":true,"handshakeS":83,"rx":97537104,"tx":13407600},{"alias":"m4mh","online":true,"handshakeS":104,"rx":125724,"tx":34132},{"alias":"m1m16b","online":true,"handshakeS":99,"rx":125396,"tx":33948},{"alias":"m4mj","online":true,"handshakeS":11,"rx":45676,"tx":12420},{"alias":"m4mk","online":true,"handshakeS":122,"rx":45368,"tx":12328},{"alias":"m4mi","online":true,"handshakeS":100,"rx":45272,"tx":12328},{"alias":"M2M32","online":true,"handshakeS":138,"rx":940560,"tx":92636},{"alias":"M1M64","online":true,"handshakeS":69,"rx":10992,"tx":20240}],"routes":36},{"name":"R9128","ok":true,"primary":false,"site":"Gravelines (France)","peers":[{"alias":"M1M32","online":true,"handshakeS":82,"rx":230344,"tx":58680},{"alias":"M4M64b","online":true,"handshakeS":97,"rx":217840,"tx":62304},{"alias":"M3U96a","online":true,"handshakeS":49,"rx":409543524,"tx":13225228}],"routes":2}]} | |
| \ No newline at end of file | ||
added
dossier/map-eu.png
+0 −0
Binary file not shown.
added
dossier/map.png
+0 −0
Binary file not shown.
added
dossier/print.mjs
+24 −0
@@ -0,0 +1,24 @@ | ||
| 1 | +// Rendu PDF du dossier : cover.html (pleine page, sans en-tête) + body.html (en-tête / pied avec pagination). | |
| 2 | +// Lancer depuis un dossier où Playwright est installé (ex. ~/Desktop/uqo-eval) : node <chemin>/print.mjs | |
| 3 | +import { chromium } from "playwright"; | |
| 4 | +import path from "node:path"; | |
| 5 | +import { fileURLToPath } from "node:url"; | |
| 6 | +const here = path.dirname(fileURLToPath(import.meta.url)); | |
| 7 | +const b = await chromium.launch(); | |
| 8 | +const p = await b.newPage(); | |
| 9 | + | |
| 10 | +await p.goto("file://" + path.join(here, "cover.html"), { waitUntil: "networkidle" }); | |
| 11 | +await p.pdf({ path: path.join(here, "cover.pdf"), width: "297mm", height: "210mm", printBackground: true, margin: { top: 0, right: 0, bottom: 0, left: 0 }, preferCSSPageSize: false }); | |
| 12 | + | |
| 13 | +await p.goto("file://" + path.join(here, "body.html"), { waitUntil: "networkidle" }); | |
| 14 | +await p.waitForTimeout(500); | |
| 15 | +const font = "font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text',Helvetica,Arial,sans-serif;font-size:7.4pt;color:#868b99;"; | |
| 16 | +await p.pdf({ | |
| 17 | + path: path.join(here, "body.pdf"), format: "A4", landscape: true, printBackground: true, preferCSSPageSize: false, | |
| 18 | + margin: { top: "13mm", right: "14mm", bottom: "15mm", left: "14mm" }, | |
| 19 | + displayHeaderFooter: true, | |
| 20 | + headerTemplate: `<div style="${font}width:100%;padding:0 14mm;display:flex;justify-content:space-between;margin-top:4mm"><span style="font-weight:600;color:#0f1115">MacLustr</span><span>Dossier technique · inventaire complet</span></div>`, | |
| 21 | + footerTemplate: `<div style="${font}width:100%;padding:0 14mm;display:flex;justify-content:space-between;margin-bottom:5mm"><span>www.maclustr.io · contact@spboucher.ai</span><span>page <span class="pageNumber"></span> / <span class="totalPages"></span></span></div>`, | |
| 22 | +}); | |
| 23 | +await b.close(); | |
| 24 | +console.log("cover.pdf + body.pdf OK"); | |
added
inventaire-maclustr.css
+27 −0
@@ -0,0 +1,27 @@ | ||
| 1 | +@page { size: A4 landscape; margin: 14mm 14mm 16mm 14mm; } | |
| 2 | +body { font: 10.5pt/1.45 -apple-system, "Helvetica Neue", Arial, sans-serif; color: #111; max-width: none; margin: 0; } | |
| 3 | +header#title-block-header { border-bottom: 2px solid #111; padding-bottom: 8px; margin-bottom: 18px; } | |
| 4 | +h1.title { font-size: 24pt; margin: 0 0 4px; letter-spacing: -.01em; } | |
| 5 | +p.subtitle { font-size: 13pt; color: #444; margin: 0 0 6px; } | |
| 6 | +p.author { font-size: 10pt; color: #666; margin: 0; } | |
| 7 | +h2 { font-size: 15pt; margin: 22px 0 8px; border-bottom: 1px solid #bbb; padding-bottom: 3px; break-after: avoid; } | |
| 8 | +h3 { font-size: 12pt; margin: 16px 0 6px; color: #222; break-after: avoid; } | |
| 9 | +p { margin: 6px 0; } | |
| 10 | +table { border-collapse: collapse; width: 100%; font-size: 9pt; margin: 8px 0 12px; break-inside: auto; } | |
| 11 | +th, td { border: 1px solid #ccc; padding: 4px 6px; vertical-align: top; text-align: left; } | |
| 12 | +th { background: #f0f0f2; font-weight: 600; } | |
| 13 | +tr { break-inside: avoid; } | |
| 14 | +tbody tr:nth-child(even) td { background: #fafafa; } | |
| 15 | +code { font: 8.5pt "SF Mono", Menlo, monospace; background: #f3f3f5; padding: 0 3px; border-radius: 3px; } | |
| 16 | +strong { font-weight: 700; } | |
| 17 | +colgroup col { width: auto !important; } | |
| 18 | +table { width: 100%; table-layout: auto; } | |
| 19 | +td, th { hyphens: none; } | |
| 20 | +td:first-child { white-space: nowrap; } | |
| 21 | +/* Inventaire complet MacLustr : toutes les cellules peuvent se replier, seul l'alias reste sur une ligne */ | |
| 22 | +td { white-space: normal; overflow-wrap: break-word; } | |
| 23 | +th { white-space: nowrap; } | |
| 24 | +td:first-child { white-space: nowrap; } | |
| 25 | +table { font-size: 8.3pt; } | |
| 26 | +code { font-size: 7.8pt; } | |
| 27 | +h2 { break-before: auto; } | |
added
inventaire.css
+21 −0
@@ -0,0 +1,21 @@ | ||
| 1 | +@page { size: A4 landscape; margin: 14mm 14mm 16mm 14mm; } | |
| 2 | +body { font: 10.5pt/1.45 -apple-system, "Helvetica Neue", Arial, sans-serif; color: #111; max-width: none; margin: 0; } | |
| 3 | +header#title-block-header { border-bottom: 2px solid #111; padding-bottom: 8px; margin-bottom: 18px; } | |
| 4 | +h1.title { font-size: 24pt; margin: 0 0 4px; letter-spacing: -.01em; } | |
| 5 | +p.subtitle { font-size: 13pt; color: #444; margin: 0 0 6px; } | |
| 6 | +p.author { font-size: 10pt; color: #666; margin: 0; } | |
| 7 | +h2 { font-size: 15pt; margin: 22px 0 8px; border-bottom: 1px solid #bbb; padding-bottom: 3px; break-after: avoid; } | |
| 8 | +h3 { font-size: 12pt; margin: 16px 0 6px; color: #222; break-after: avoid; } | |
| 9 | +p { margin: 6px 0; } | |
| 10 | +table { border-collapse: collapse; width: 100%; font-size: 9pt; margin: 8px 0 12px; break-inside: auto; } | |
| 11 | +th, td { border: 1px solid #ccc; padding: 4px 6px; vertical-align: top; text-align: left; } | |
| 12 | +th { background: #f0f0f2; font-weight: 600; } | |
| 13 | +tr { break-inside: avoid; } | |
| 14 | +tbody tr:nth-child(even) td { background: #fafafa; } | |
| 15 | +code { font: 8.5pt "SF Mono", Menlo, monospace; background: #f3f3f5; padding: 0 3px; border-radius: 3px; } | |
| 16 | +strong { font-weight: 700; } | |
| 17 | +colgroup col { width: auto !important; } | |
| 18 | +table { width: 100%; table-layout: auto; } | |
| 19 | +td, th { hyphens: none; } | |
| 20 | +td:nth-child(n+4):nth-child(-n+6):not(:last-child) { white-space: nowrap; } | |
| 21 | +td:first-child { white-space: nowrap; } | |
added
maclustr-tunnel/Caddyfile
+34 −0
@@ -0,0 +1,34 @@ | ||
| 1 | +{ | |
| 2 | + email contact@spboucher.ai | |
| 3 | + admin localhost:2019 | |
| 4 | + servers { | |
| 5 | + trusted_proxies static private_ranges | |
| 6 | + } | |
| 7 | +} | |
| 8 | + | |
| 9 | +# Pages d'erreur MacLustr (app injoignable derrière le tunnel) — importé par chaque route de tunnelctl. | |
| 10 | +(maclustr_errors) { | |
| 11 | + handle_errors 502 503 504 { | |
| 12 | + root * /etc/caddy/errors | |
| 13 | + rewrite * /down.html | |
| 14 | + templates | |
| 15 | + file_server { | |
| 16 | + status {err.status_code} | |
| 17 | + } | |
| 18 | + header Cache-Control "no-store" | |
| 19 | + header Retry-After "20" | |
| 20 | + } | |
| 21 | +} | |
| 22 | + | |
| 23 | +# Hôtes inconnus : page 404 MacLustr (évite de servir une route au hasard). | |
| 24 | +:80, :443 { | |
| 25 | + root * /etc/caddy/errors | |
| 26 | + rewrite * /unknown.html | |
| 27 | + templates | |
| 28 | + file_server { | |
| 29 | + status 404 | |
| 30 | + } | |
| 31 | + header Cache-Control "no-store" | |
| 32 | +} | |
| 33 | + | |
| 34 | +import sites/*.caddy | |
added
maclustr-tunnel/errors/down.html
+50 −0
@@ -0,0 +1,50 @@ | ||
| 1 | +<!doctype html> | |
| 2 | +<html lang="fr"> | |
| 3 | +<head> | |
| 4 | +<meta charset="utf-8"> | |
| 5 | +<meta name="viewport" content="width=device-width,initial-scale=1"> | |
| 6 | +<meta http-equiv="refresh" content="20"> | |
| 7 | +<title>{{placeholder "http.request.host"}} — service momentanément indisponible</title> | |
| 8 | +<meta name="robots" content="noindex"> | |
| 9 | +<style> | |
| 10 | +:root{--bg:#07070b;--fg:#f2f2f6;--muted:#9a9aab;--dim:#6c6c7c;--line:rgba(255,255,255,.1);--grad:linear-gradient(120deg,#ff6a3d,#f43f8e 45%,#a855f7)} | |
| 11 | +*{box-sizing:border-box} | |
| 12 | +body{margin:0;min-height:100vh;display:grid;place-items:center;background:var(--bg);color:var(--fg);font:16px/1.6 -apple-system,"Space Grotesk",system-ui,sans-serif;overflow:hidden} | |
| 13 | +.glow{position:fixed;inset:-30%;background:radial-gradient(closest-side at 30% 30%,rgba(255,106,61,.25),transparent 60%),radial-gradient(closest-side at 70% 65%,rgba(168,85,247,.25),transparent 60%),radial-gradient(closest-side at 55% 20%,rgba(34,211,238,.15),transparent 60%);filter:blur(30px);animation:drift 18s ease-in-out infinite alternate;pointer-events:none} | |
| 14 | +@keyframes drift{to{transform:translate(4%,3%) scale(1.05)}} | |
| 15 | +main{position:relative;max-width:620px;margin:24px;padding:40px 40px 32px;border:1px solid var(--line);border-radius:28px;background:rgba(255,255,255,.035);backdrop-filter:blur(16px);box-shadow:0 40px 100px -40px rgba(168,85,247,.45)} | |
| 16 | +.brand{display:flex;align-items:center;gap:10px;font-weight:700;letter-spacing:.02em;color:var(--muted);font-size:14px;text-transform:uppercase} | |
| 17 | +.orb{width:14px;height:14px;border-radius:50%;background:var(--grad);box-shadow:0 0 18px rgba(255,106,61,.7)} | |
| 18 | +h1{font-family:"Instrument Serif",Georgia,serif;font-weight:400;font-size:clamp(34px,6vw,50px);line-height:1.05;letter-spacing:-.02em;margin:22px 0 14px} | |
| 19 | +h1 em{font-style:italic;background:var(--grad);-webkit-background-clip:text;background-clip:text;color:transparent} | |
| 20 | +p{margin:0 0 14px;color:var(--muted)} | |
| 21 | +code{font:13px ui-monospace,Menlo,monospace;background:rgba(255,255,255,.08);padding:2px 8px;border-radius:8px;color:var(--fg)} | |
| 22 | +.row{display:flex;flex-wrap:wrap;gap:12px;align-items:center;margin-top:26px} | |
| 23 | +.btn{display:inline-flex;align-items:center;gap:8px;padding:12px 20px;border-radius:999px;border:1px solid var(--line);color:var(--fg);text-decoration:none;font-weight:600;font-size:14.5px;background:rgba(255,255,255,.04)} | |
| 24 | +.btn.primary{background:var(--grad);border-color:transparent;box-shadow:0 8px 32px -8px rgba(244,63,142,.6)} | |
| 25 | +.meta{margin-top:26px;padding-top:16px;border-top:1px solid var(--line);font:12.5px ui-monospace,Menlo,monospace;color:var(--dim);display:flex;flex-wrap:wrap;gap:8px 18px} | |
| 26 | +.pulse{display:inline-block;width:8px;height:8px;border-radius:50%;background:#fbbf24;animation:ping 1.6s infinite;margin-right:8px;vertical-align:middle} | |
| 27 | +@keyframes ping{0%{box-shadow:0 0 0 0 rgba(251,191,36,.6)}70%{box-shadow:0 0 0 9px rgba(251,191,36,0)}100%{box-shadow:0 0 0 0 rgba(251,191,36,0)}} | |
| 28 | +</style> | |
| 29 | +</head> | |
| 30 | +<body> | |
| 31 | +<div class="glow"></div> | |
| 32 | +<main> | |
| 33 | + <div class="brand"><span class="orb"></span> MacLustr</div> | |
| 34 | + <h1>Ce service est <em>momentanément</em> indisponible.</h1> | |
| 35 | + <p><span class="pulse"></span>L'application derrière <code>{{placeholder "http.request.host"}}</code> ne répond pas pour l'instant. Elle redémarre peut-être, ou le nœud qui l'héberge est en maintenance. Cette page réessaie automatiquement toutes les 20 secondes.</p> | |
| 36 | + <p>Si le problème persiste, écris-nous : nous regardons rapidement.</p> | |
| 37 | + <div class="row"> | |
| 38 | + <a class="btn primary" href="mailto:contact@spboucher.ai?subject=Service%20indisponible%20%E2%80%94%20{{placeholder "http.request.host"}}">✉︎ contact@spboucher.ai</a> | |
| 39 | + <a class="btn" href="javascript:location.reload()">Réessayer maintenant</a> | |
| 40 | + </div> | |
| 41 | + <div class="meta"> | |
| 42 | + <span>erreur {{placeholder "http.error.status_code"}}</span> | |
| 43 | + <span>{{placeholder "http.request.host"}}</span> | |
| 44 | + <span id="t"></span> | |
| 45 | + <span>passerelle MacLustr Tunnel</span> | |
| 46 | + </div> | |
| 47 | +</main> | |
| 48 | +<script>document.getElementById('t').textContent=new Date().toLocaleString('fr-CA');</script> | |
| 49 | +</body> | |
| 50 | +</html> | |
added
maclustr-tunnel/errors/unknown.html
+34 −0
@@ -0,0 +1,34 @@ | ||
| 1 | +<!doctype html> | |
| 2 | +<html lang="fr"> | |
| 3 | +<head> | |
| 4 | +<meta charset="utf-8"> | |
| 5 | +<meta name="viewport" content="width=device-width,initial-scale=1"> | |
| 6 | +<title>MacLustr — hôte inconnu</title> | |
| 7 | +<meta name="robots" content="noindex"> | |
| 8 | +<style> | |
| 9 | +:root{--bg:#07070b;--fg:#f2f2f6;--muted:#9a9aab;--dim:#6c6c7c;--line:rgba(255,255,255,.1);--grad:linear-gradient(120deg,#ff6a3d,#f43f8e 45%,#a855f7)} | |
| 10 | +*{box-sizing:border-box} | |
| 11 | +body{margin:0;min-height:100vh;display:grid;place-items:center;background:var(--bg);color:var(--fg);font:16px/1.6 -apple-system,system-ui,sans-serif;overflow:hidden} | |
| 12 | +.glow{position:fixed;inset:-30%;background:radial-gradient(closest-side at 30% 30%,rgba(255,106,61,.2),transparent 60%),radial-gradient(closest-side at 70% 65%,rgba(168,85,247,.22),transparent 60%);filter:blur(30px);pointer-events:none} | |
| 13 | +main{position:relative;max-width:560px;margin:24px;padding:40px;border:1px solid var(--line);border-radius:28px;background:rgba(255,255,255,.035);backdrop-filter:blur(16px)} | |
| 14 | +.brand{display:flex;align-items:center;gap:10px;font-weight:700;letter-spacing:.02em;color:var(--muted);font-size:14px;text-transform:uppercase} | |
| 15 | +.orb{width:14px;height:14px;border-radius:50%;background:var(--grad);box-shadow:0 0 18px rgba(255,106,61,.7)} | |
| 16 | +h1{font-family:"Instrument Serif",Georgia,serif;font-weight:400;font-size:clamp(32px,6vw,46px);line-height:1.05;letter-spacing:-.02em;margin:22px 0 14px} | |
| 17 | +p{margin:0 0 14px;color:var(--muted)} | |
| 18 | +code{font:13px ui-monospace,Menlo,monospace;background:rgba(255,255,255,.08);padding:2px 8px;border-radius:8px;color:var(--fg)} | |
| 19 | +a.btn{display:inline-flex;align-items:center;gap:8px;padding:12px 20px;border-radius:999px;border:0;color:#fff;text-decoration:none;font-weight:600;font-size:14.5px;background:var(--grad);margin-top:14px} | |
| 20 | +.meta{margin-top:26px;padding-top:16px;border-top:1px solid var(--line);font:12.5px ui-monospace,Menlo,monospace;color:var(--dim)} | |
| 21 | +</style> | |
| 22 | +</head> | |
| 23 | +<body> | |
| 24 | +<div class="glow"></div> | |
| 25 | +<main> | |
| 26 | + <div class="brand"><span class="orb"></span> MacLustr</div> | |
| 27 | + <h1>Aucun service ne répond à ce nom.</h1> | |
| 28 | + <p>L'hôte <code>{{placeholder "http.request.host"}}</code> pointe vers la passerelle MacLustr, mais aucune application n'y est rattachée.</p> | |
| 29 | + <p>Si tu penses que c'est une erreur, écris-nous.</p> | |
| 30 | + <a class="btn" href="mailto:contact@spboucher.ai?subject=H%C3%B4te%20inconnu%20%E2%80%94%20{{placeholder "http.request.host"}}">✉︎ contact@spboucher.ai</a> | |
| 31 | + <div class="meta">erreur 404 · passerelle MacLustr Tunnel</div> | |
| 32 | +</main> | |
| 33 | +</body> | |
| 34 | +</html> | |
added
maclustr-tunnel/gateway-setup.sh
+32 −0
@@ -0,0 +1,32 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +# gateway-setup.sh — transforme un Ubuntu 24.04 fraîchement installé (OVH) en passerelle MacLustr Tunnel : | |
| 3 | +# WireGuard hub + Caddy (TLS auto) + ufw + tunnelctl + pages d'erreur MacLustr. | |
| 4 | +# Usage (depuis le laptop) : bash gateway-setup.sh <alias-ssh> <ip-publique> <sous-réseau wg, ex. 10.67.0> | |
| 5 | +# Le hub prend <sous-réseau>.1 ; la carte alias→IP est dérivée de ipmap en remplaçant 10.66.0 par le sous-réseau. | |
| 6 | +set -euo pipefail | |
| 7 | +GW=${1:?alias ssh}; PUB_IP=${2:?ip publique}; NET=${3:-10.66.0} | |
| 8 | +HERE="$(cd "$(dirname "$0")" && pwd)" | |
| 9 | +echo "== paquets" | |
| 10 | +ssh "$GW" 'set -e; sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq wireguard wireguard-tools ufw bc debian-keyring debian-archive-keyring apt-transport-https curl >/dev/null 2>&1 | |
| 11 | + curl -1sLf https://dl.cloudsmith.io/public/caddy/stable/gpg.key | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg --yes | |
| 12 | + curl -1sLf https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt | sudo tee /etc/apt/sources.list.d/caddy-stable.list >/dev/null | |
| 13 | + sudo apt-get update -qq >/dev/null 2>&1; sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq caddy >/dev/null 2>&1 | |
| 14 | + wg --version | head -1; caddy version' | |
| 15 | +echo "== wireguard hub $NET.1" | |
| 16 | +ssh "$GW" "set -e; sudo mkdir -p /etc/wireguard/peers.d /etc/maclustr-tunnel /etc/caddy/sites /etc/caddy/errors | |
| 17 | + if [ ! -f /etc/wireguard/server.key ]; then umask 077; wg genkey | sudo tee /etc/wireguard/server.key >/dev/null; sudo cat /etc/wireguard/server.key | wg pubkey | sudo tee /etc/wireguard/server.pub >/dev/null; fi | |
| 18 | + sudo chmod 600 /etc/wireguard/server.key | |
| 19 | + [ -f /etc/wireguard/wg0.conf ] || printf '[Interface]\nAddress = $NET.1/24\nListenPort = 51820\nPrivateKey = %s\n# [Peer] gérés par tunnelctl (peers.d/*.conf)\n' \"\$(sudo cat /etc/wireguard/server.key)\" | sudo tee /etc/wireguard/wg0.conf >/dev/null | |
| 20 | + sudo chmod 600 /etc/wireguard/wg0.conf; sudo systemctl enable --now wg-quick@wg0 >/dev/null 2>&1 || sudo systemctl restart wg-quick@wg0 | |
| 21 | + echo \"pubkey hub: \$(sudo cat /etc/wireguard/server.pub)\"" | |
| 22 | +echo "== tunnelctl, ipmap, Caddyfile, pages d'erreur" | |
| 23 | +sed "s/10\.66\.0\./$NET./g" "$HERE/ipmap" > /tmp/ipmap.gw | |
| 24 | +scp -q "$HERE/tunnelctl" "$HERE/Caddyfile" /tmp/ipmap.gw "$GW:/tmp/" | |
| 25 | +scp -q -r "$HERE/errors" "$GW:/tmp/errors" | |
| 26 | +ssh "$GW" "set -e; sudo install -m 755 /tmp/tunnelctl /usr/local/bin/tunnelctl; echo $PUB_IP | sudo tee /etc/maclustr-tunnel/public_ip >/dev/null | |
| 27 | + sudo install -m 644 /tmp/ipmap.gw /etc/maclustr-tunnel/ipmap; sudo install -m 644 /tmp/Caddyfile /etc/caddy/Caddyfile; sudo install -m 644 /tmp/errors/*.html /etc/caddy/errors/ | |
| 28 | + sudo caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile 2>&1 | tail -1; sudo systemctl enable caddy >/dev/null 2>&1; sudo systemctl reload caddy || sudo systemctl restart caddy" | |
| 29 | +echo "== ufw" | |
| 30 | +ssh "$GW" 'sudo ufw --force reset >/dev/null; sudo ufw default deny incoming >/dev/null; sudo ufw default allow outgoing >/dev/null; for r in 22/tcp 80/tcp 443/tcp 443/udp 51820/udp; do sudo ufw allow $r >/dev/null; done; sudo ufw --force enable >/dev/null; sudo ufw status | head -3' | |
| 31 | +echo "== état" | |
| 32 | +ssh "$GW" 'sudo tunnelctl status' | |
added
maclustr-tunnel/hub-BHS64.pub
+1 −0
@@ -0,0 +1 @@ | ||
| 1 | +QrpSXW8mIJjGMuB3eiP2LQJGpFgV6lTmBG9v0Cxsiz0= | |
added
maclustr-tunnel/hub-R9128.pub
+1 −0
@@ -0,0 +1 @@ | ||
| 1 | +JxZf0TRZPpYl8z4fKDPIsFzReTfpNeMH8h5yZHYThCc= | |
added
maclustr-tunnel/ipmap
+34 −0
@@ -0,0 +1,34 @@ | ||
| 1 | +# alias dernier octet WireGuard (10.67.0.x sur BHS64 depuis le 2026-10-02 ; historiquement 10.66.0.x sur R9128, retiré). Plage 10.66/16 choisie car les Macs loués Macly sont en 10.100.0.0/16. | |
| 2 | +M1M32 10.66.0.2 | |
| 3 | +M3U96a 10.66.0.10 | |
| 4 | +M3U96b 10.66.0.11 | |
| 5 | +M2U64 10.66.0.12 | |
| 6 | +M4M64a 10.66.0.13 | |
| 7 | +M4M64b 10.66.0.14 | |
| 8 | +M4BP48 10.66.0.15 | |
| 9 | +M4BP36 10.66.0.16 | |
| 10 | +M4M36 10.66.0.17 | |
| 11 | +M2M32 10.66.0.18 | |
| 12 | +M2M32b 10.66.0.19 | |
| 13 | +M2M32c 10.66.0.20 | |
| 14 | +m4mc 10.66.0.21 | |
| 15 | +m4ma 10.66.0.22 | |
| 16 | +m4mb 10.66.0.23 | |
| 17 | +m4md 10.66.0.24 | |
| 18 | +m2m16 10.66.0.25 | |
| 19 | +M3BA24 10.66.0.26 | |
| 20 | +M3BA16 10.66.0.27 | |
| 21 | +m2m8a 10.66.0.28 | |
| 22 | +m2m8b 10.66.0.29 | |
| 23 | +M1M64 10.66.0.40 | |
| 24 | +m2m16b 10.66.0.41 | |
| 25 | +m1m16 10.66.0.42 | |
| 26 | +m2m16c 10.66.0.43 | |
| 27 | +m4me 10.66.0.44 | |
| 28 | +m4mf 10.66.0.45 | |
| 29 | +m4mg 10.66.0.46 | |
| 30 | +m4mh 10.66.0.47 | |
| 31 | +m1m16b 10.66.0.48 | |
| 32 | +m4mi 10.66.0.49 | |
| 33 | +m4mj 10.66.0.50 | |
| 34 | +m4mk 10.66.0.51 | |
added
maclustr-tunnel/migrate-watch.sh
+39 −0
@@ -0,0 +1,39 @@ | ||
| 1 | +#!/usr/bin/env bash | |
| 2 | +# migrate-watch.sh — surveille une liste de domaines ; dès que le DNS autoritaire pointe vers la passerelle, | |
| 3 | +# crée la route Caddy (+ redirection apex si l'apex pointe aussi vers la passerelle) et vérifie en HTTPS. | |
| 4 | +# Usage : migrate-watch.sh <fichier-liste> [passerelle=BHS64] [durée max en s=7200] | |
| 5 | +# fichier-liste : une ligne par site « <domaine> <alias>:<port> [<alias>:<port>…] » (# = commentaire) | |
| 6 | +# Journal : ~/Desktop/cluster-skill/maclustr-tunnel/migrate.log | |
| 7 | +set -uo pipefail | |
| 8 | +LIST=${1:?liste}; GW=${2:-BHS64}; MAX=${3:-7200} | |
| 9 | +HERE="$(cd "$(dirname "$0")" && pwd)"; MLT="$HERE/../mlt"; LOG="$HERE/migrate.log" | |
| 10 | +case "$GW" in BHS64) IP=51.161.112.61;; R9128) IP=51.255.75.61;; *) echo "passerelle ?"; exit 1;; esac | |
| 11 | +DONE_DIR=$(mktemp -d) | |
| 12 | +log(){ echo "$(date '+%F %T') $*" | tee -a "$LOG"; } | |
| 13 | +apex_of(){ echo "$1" | sed -E 's/^(www|app|console|node|cloud|git)\.//'; } | |
| 14 | +t0=$(date +%s) | |
| 15 | +log "== surveillance de $(grep -c -v -E '^\s*(#|$)' "$LIST") domaines → $GW ($IP)" | |
| 16 | +while :; do | |
| 17 | + left=0 | |
| 18 | + while read -r dom ups; do | |
| 19 | + [[ -z "$dom" || "$dom" =~ ^# ]] && continue | |
| 20 | + [ -f "$DONE_DIR/$dom" ] && continue | |
| 21 | + left=$((left+1)) | |
| 22 | + apex=$(apex_of "$dom"); ns=$(dig +short NS "$apex" | head -1) | |
| 23 | + a=$(dig +short "$dom" A @"$ns" 2>/dev/null | tail -1) | |
| 24 | + [ "$a" = "$IP" ] || continue | |
| 25 | + log "→ $dom pointe vers $GW : création de la route ($ups)" | |
| 26 | + "$MLT" -g "$GW" add "$dom" $ups 2>&1 | grep -v '^{' | sed 's/^/ /' | tee -a "$LOG" | |
| 27 | + if [ "$apex" != "$dom" ]; then | |
| 28 | + aa=$(dig +short "$apex" A @"$ns" 2>/dev/null | tail -1) | |
| 29 | + if [ "$aa" = "$IP" ]; then "$MLT" -g "$GW" redirect "$apex" "$dom" 2>&1 | grep -v '^{' | sed 's/^/ /' | tee -a "$LOG"; else log " apex $apex = ${aa:-∅} (pas de redirection posée)"; fi | |
| 30 | + fi | |
| 31 | + sleep 8 | |
| 32 | + code=$(/usr/bin/curl -s -m 40 --resolve "$dom:443:$IP" -o /dev/null -w '%{http_code}' "https://$dom/") | |
| 33 | + log " https://$dom via $GW → HTTP $code" | |
| 34 | + touch "$DONE_DIR/$dom" | |
| 35 | + done < "$LIST" | |
| 36 | + [ $left -eq 0 ] && { log "== tous les domaines migrés"; break; } | |
| 37 | + [ $(( $(date +%s) - t0 )) -gt "$MAX" ] && { log "== délai dépassé, restent $left domaine(s)"; break; } | |
| 38 | + sleep 20 | |
| 39 | +done | |
added
maclustr-tunnel/ngrok-map.txt
+34 −0
@@ -0,0 +1,34 @@ | ||
| 1 | +# domaine nœud app ngrok(pm2:<nom>|launchd:<label>|?) | |
| 2 | +app.maclustr.io M2U64 maclustr-appmanager pm2:app-ngrok | |
| 3 | +cloud.spboucher.ai M2U64 spb-cloud pm2:cloud-ngrok | |
| 4 | +console.maclustr.io M2U64 maclustr-console pm2:console-ngrok | |
| 5 | +git.spboucher.ai M1M32 spbgit pm2:spbgit-tunnel | |
| 6 | +node.maclustr.io M2U64 maclustr-nodepages pm2:node-ngrok | |
| 7 | +www.administration-ka.com M3U96a admin-ka launchd:io.adminka.ngrok | |
| 8 | +www.api-ka.com M4M64b api-ka pm2:apika-ngrok | |
| 9 | +www.auto-ka.com M4M64b auto-ka pm2:auto-ka-ngrok | |
| 10 | +www.cancerindex.io M4M64b cancerindex pm2:cancerindex-ngrok | |
| 11 | +www.fabri-ka.com M4M64a fabri-ka pm2:fabri-ka-ngrok | |
| 12 | +www.fetcha.co M3U96a fetcha pm2:fetcha-ngrok | |
| 13 | +www.food-ka.com M2U64 food-ka pm2:food-ka-ngrok | |
| 14 | +www.groupe-ka.com M4M64b groupe-ka pm2:groupe-ka-ngrok | |
| 15 | +www.hfmarketdata.io M3U96b hfmarketdata pm2:hfmarketdata-ngrok | |
| 16 | +www.immo-ka.com M4M64b immo-ka pm2:immo-ka-ngrok | |
| 17 | +www.job-ka.com M3U96a job-ka pm2:job-ka-ngrok | |
| 18 | +www.ka-stats.com M4M64a ka-stats pm2:ka-stats-ngrok | |
| 19 | +www.ka2.bot M4M36 ka2 launchd:com.ka2.ngrok | |
| 20 | +www.ka4.bot M4M36 ka4 launchd:com.ka4.ngrok | |
| 21 | +www.ka6.bot M4M36 ka6 launchd:com.ka6.ngrok | |
| 22 | +www.lou-ka.com M4M64a lou-ka pm2:lou-ka-ngrok | |
| 23 | +www.pdb-api.co M4M64a pdb-api pm2:pdb-api-ngrok | |
| 24 | +www.polyllm.io M3U96a polyllm pm2:polyllm-ngrok | |
| 25 | +www.rareindex.io M3U96a rareindex pm2:rareindex-ngrok | |
| 26 | +www.sorti-ka.com M3U96a sorti-ka pm2:sorti-ka-ngrok | |
| 27 | +www.spboucher.ai M2U64 spboucher-ai pm2:spboucher-ngrok | |
| 28 | +www.uqo-chat.app M4M64a uqo-chat pm2:uqo-chat-ngrok | |
| 29 | +www.uqo-eval.app M2U64 uqo-eval pm2:uqo-eval-ngrok | |
| 30 | +www.uqo-imm1003.app M4M64b uqo-imm1003 pm2:uqo-imm1003-ngrok | |
| 31 | +www.uqo-imm1033.app M3U96a uqo-imm1033 pm2:uqo-imm1033-ngrok | |
| 32 | +www.vquant.ai M2U64 vquant pm2:vquant-ngrok | |
| 33 | +www.vrai-prix.com M3U96a vrai-prix pm2:vrai-prix-ngrok | |
| 34 | +www.websensor.io M4M64b websensor pm2:websensor-ngrok | |
added
maclustr-tunnel/ngrok-retire.sh
+47 −0
@@ -0,0 +1,47 @@ | ||
| 1 | +#!/usr/bin/env bash | |
| 2 | +# ngrok-retire.sh — retire les tunnels ngrok des sites migrés vers le MacLustr Tunnel, un par un, dès que les | |
| 3 | +# résolveurs publics (8.8.8.8 et 1.1.1.1) renvoient l'IP de la passerelle. Met aussi le manifeste mld à jour | |
| 4 | +# (ngrok → null, launchd ngrok retiré, champ tunnel) pour qu'un `mld deploy` ne relance pas ngrok. | |
| 5 | +# Usage : ngrok-retire.sh <ngrok-map.txt> <sites.txt> [passerelle=BHS64] [durée max s=10800] | |
| 6 | +# Journal : maclustr-tunnel/ngrok-retire.log | |
| 7 | +set -uo pipefail | |
| 8 | +MAP=${1:?map}; SITES=${2:?sites}; GW=${3:-BHS64}; MAX=${4:-10800} | |
| 9 | +HERE="$(cd "$(dirname "$0")" && pwd)"; LOG="$HERE/ngrok-retire.log"; DONE=$(mktemp -d) | |
| 10 | +case "$GW" in BHS64) IP=51.161.112.61;; R9128) IP=51.255.75.61;; esac | |
| 11 | +log(){ echo "$(date '+%F %T') $*" | tee -a "$LOG"; } | |
| 12 | +t0=$(date +%s) | |
| 13 | +log "== retrait ngrok pour $(grep -c -v -E '^\s*(#|$)' "$MAP") sites (quand 8.8.8.8 + 1.1.1.1 → $IP)" | |
| 14 | +while :; do | |
| 15 | + left=0 | |
| 16 | + while read -r dom node app kind; do | |
| 17 | + [[ -z "$dom" || "$dom" =~ ^# ]] && continue | |
| 18 | + [ -f "$DONE/$dom" ] && continue | |
| 19 | + left=$((left+1)) | |
| 20 | + g=$(dig +short "$dom" A @8.8.8.8 | tail -1); c=$(dig +short "$dom" A @1.1.1.1 | tail -1) | |
| 21 | + [ "$g" = "$IP" ] && [ "$c" = "$IP" ] || continue | |
| 22 | + ups=$(awk -v d="$dom" '$1==d{ $1=""; print }' "$SITES" | sed 's/^ *//') | |
| 23 | + log "→ $dom : caches publics OK → retrait ngrok ($kind sur $node)" | |
| 24 | + case "$kind" in | |
| 25 | + pm2:*) n=${kind#pm2:}; ssh -o ConnectTimeout=20 "$node" "export PATH=/opt/homebrew/bin:\$PATH; pm2 delete $n >/dev/null 2>&1 && pm2 save >/dev/null 2>&1 && echo ' pm2 $n supprimé' || echo ' pm2 $n : déjà absent'" 2>&1 | tee -a "$LOG" ;; | |
| 26 | + launchd:*) l=${kind#launchd:}; ssh -o ConnectTimeout=20 "$node" "launchctl bootout gui/\$(id -u)/$l >/dev/null 2>&1; [ -f ~/Library/LaunchAgents/$l.plist ] && mv ~/Library/LaunchAgents/$l.plist ~/Library/LaunchAgents/$l.plist.disabled-ngrok-\$(date +%F) && echo ' launchd $l déchargé, plist désactivé' || echo ' launchd $l : plist absent'" 2>&1 | tee -a "$LOG" ;; | |
| 27 | + *) log " type inconnu « $kind », rien fait sur le nœud" ;; | |
| 28 | + esac | |
| 29 | + ssh M1M32 "python3 - '$app' '$dom' '$ups' '$GW'" <<'PY' 2>&1 | tee -a "$LOG" | |
| 30 | +import json,sys,shutil,os,datetime | |
| 31 | +app,dom,ups,gw=sys.argv[1:5] | |
| 32 | +p=os.path.expanduser(f"~/dispatch/apps/{app}.json") | |
| 33 | +if not os.path.exists(p): print(f" manifeste {app}.json absent"); sys.exit() | |
| 34 | +shutil.copy(p,p+".bak-ngrok-"+datetime.date.today().isoformat()) | |
| 35 | +m=json.load(open(p)); m["ngrok"]=None | |
| 36 | +if isinstance(m.get("launchd"),list): m["launchd"]=[l for l in m["launchd"] if "ngrok" not in json.dumps(l).lower()] | |
| 37 | +m["tunnel"]={"domain":dom,"upstreams":ups.split(),"gateway":gw,"note":"MacLustr Tunnel depuis 2026-09-10 ; ngrok retiré"} | |
| 38 | +json.dump(m,open(p,"w"),indent=2,ensure_ascii=False); print(f" manifeste {app}.json : ngrok=null, tunnel={dom}") | |
| 39 | +PY | |
| 40 | + code=$(/usr/bin/curl -s -m 30 -o /dev/null -w '%{http_code}' "https://$dom/") | |
| 41 | + log " https://$dom (DNS public) → HTTP $code" | |
| 42 | + touch "$DONE/$dom" | |
| 43 | + done < "$MAP" | |
| 44 | + [ $left -eq 0 ] && { log "== ngrok retiré partout"; break; } | |
| 45 | + [ $(( $(date +%s) - t0 )) -gt "$MAX" ] && { log "== délai dépassé, restent $left site(s)"; break; } | |
| 46 | + sleep 60 | |
| 47 | +done | |
added
maclustr-tunnel/sites-ngrok.txt
+41 −0
@@ -0,0 +1,41 @@ | ||
| 1 | +# Sites ngrok à migrer vers le MacLustr Tunnel — <domaine> <nœud>:<port> (2026-09-10 ; www.qc26.ca déjà migré) | |
| 2 | +# M1M32 | |
| 3 | +git.spboucher.ai M1M32:7420 | |
| 4 | +# M2U64 | |
| 5 | +www.food-ka.com M2U64:8097 | |
| 6 | +app.maclustr.io M2U64:8787 | |
| 7 | +console.maclustr.io M2U64:3000 | |
| 8 | +node.maclustr.io M2U64:8003 | |
| 9 | +cloud.spboucher.ai M2U64:5001 | |
| 10 | +www.spboucher.ai M2U64:3100 | |
| 11 | +www.uqo-eval.app M2U64:8150 | |
| 12 | +www.vquant.ai M2U64:5000 | |
| 13 | +# M3U96a | |
| 14 | +www.administration-ka.com M3U96a:3300 | |
| 15 | +www.fetcha.co M3U96a:8220 | |
| 16 | +www.job-ka.com M3U96a:8096 | |
| 17 | +www.polyllm.io M3U96a:8240 | |
| 18 | +www.rareindex.io M3U96a:8210 | |
| 19 | +www.sorti-ka.com M3U96a:8120 | |
| 20 | +www.uqo-imm1033.app M3U96a:8161 | |
| 21 | +www.vrai-prix.com M3U96a:8090 | |
| 22 | +# M3U96b | |
| 23 | +www.hfmarketdata.io M3U96b:8090 | |
| 24 | +# M4M36 | |
| 25 | +www.ka2.bot M4M36:8799 | |
| 26 | +www.ka4.bot M4M36:8899 | |
| 27 | +www.ka6.bot M4M36:8999 | |
| 28 | +# M4M64a | |
| 29 | +www.fabri-ka.com M4M64a:8097 | |
| 30 | +www.ka-stats.com M4M64a:8140 | |
| 31 | +www.lou-ka.com M4M64a:8095 | |
| 32 | +www.pdb-api.co M4M64a:8270 | |
| 33 | +www.uqo-chat.app M4M64a:8190 | |
| 34 | +# M4M64b | |
| 35 | +www.api-ka.com M4M64b:8000 | |
| 36 | +www.auto-ka.com M4M64b:8095 | |
| 37 | +www.cancerindex.io M4M64b:8250 | |
| 38 | +www.groupe-ka.com M4M64b:8110 | |
| 39 | +www.immo-ka.com M4M64b:8096 | |
| 40 | +www.uqo-imm1003.app M4M64b:8160 | |
| 41 | +www.websensor.io M4M64b:8260 | |
added
maclustr-tunnel/tunnelctl
+123 −0
@@ -0,0 +1,123 @@ | ||
| 1 | +#!/usr/bin/env bash | |
| 2 | +# tunnelctl — MacLustr Tunnel : gestion des pairs WireGuard et des routes Caddy sur la passerelle publique (R9128). | |
| 3 | +# Usage : | |
| 4 | +# tunnelctl peer add <alias> <pubkey> ajoute/remplace un Mac (IP fixe tirée de /etc/maclustr-tunnel/ipmap) | |
| 5 | +# tunnelctl peer rm <alias> | |
| 6 | +# tunnelctl peer ls pairs + dernier handshake | |
| 7 | +# tunnelctl add <domaine> <alias|ip>:<port> [<alias|ip>:<port>…] [--no-tls] [--websocket] | |
| 8 | +# route publique https://<domaine> → upstream(s) via WireGuard (LB + health si plusieurs) | |
| 9 | +# tunnelctl redirect <domaine> <cible> redirection 308 (ex. apex → www) | |
| 10 | +# tunnelctl rm <domaine> | |
| 11 | +# tunnelctl ls routes publiques | |
| 12 | +# tunnelctl status wg + caddy + routes | |
| 13 | +# tunnelctl json même chose en JSON (pour maclustr-agentd) | |
| 14 | +set -euo pipefail | |
| 15 | +ETC=/etc/maclustr-tunnel | |
| 16 | +IPMAP=$ETC/ipmap | |
| 17 | +PEERS=/etc/wireguard/peers.d | |
| 18 | +SITES=/etc/caddy/sites | |
| 19 | +WG_IF=wg0 | |
| 20 | +PUBLIC_IP=${PUBLIC_IP:-$(cat /etc/maclustr-tunnel/public_ip 2>/dev/null || echo 51.255.75.61)} | |
| 21 | +[ "$(id -u)" = 0 ] || exec sudo -E "$0" "$@" | |
| 22 | +mkdir -p "$ETC" "$PEERS" "$SITES" | |
| 23 | + | |
| 24 | +die(){ echo "tunnelctl: $*" >&2; exit 1; } | |
| 25 | +ip_of(){ awk -v a="$1" '$1==a{print $2}' "$IPMAP"; } | |
| 26 | +alias_of(){ awk -v ip="$1" '$2==ip{print $1}' "$IPMAP"; } | |
| 27 | +resolve_upstream(){ # alias:port | ip:port -> ip:port | |
| 28 | + local hp=$1 h=${1%%:*} p=${1##*:} | |
| 29 | + [ "$h" != "$p" ] || die "upstream « $hp » : format <alias|ip>:<port>" | |
| 30 | + if [[ "$h" =~ ^[0-9.]+$ ]]; then echo "$h:$p"; else local ip; ip=$(ip_of "$h"); [ -n "$ip" ] || die "alias inconnu « $h » (voir $IPMAP)"; echo "$ip:$p"; fi | |
| 31 | +} | |
| 32 | +wg_sync(){ # reconstruit wg0.conf = [Interface] + peers.d/*.conf puis applique sans couper les tunnels | |
| 33 | + local conf=/etc/wireguard/$WG_IF.conf tmp; tmp=$(mktemp) | |
| 34 | + awk '/^\[Peer\]/{exit} {print}' "$conf" > "$tmp" | |
| 35 | + for f in "$PEERS"/*.conf; do [ -f "$f" ] && { echo; cat "$f"; } >> "$tmp"; done | |
| 36 | + install -m 600 "$tmp" "$conf"; rm -f "$tmp" | |
| 37 | + wg syncconf "$WG_IF" <(wg-quick strip "$WG_IF") | |
| 38 | +} | |
| 39 | +caddy_reload(){ caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile >/dev/null && systemctl reload caddy; } | |
| 40 | + | |
| 41 | +cmd=${1:-status}; shift || true | |
| 42 | +case "$cmd" in | |
| 43 | + peer) | |
| 44 | + sub=${1:-ls}; shift || true | |
| 45 | + case "$sub" in | |
| 46 | + add) | |
| 47 | + a=${1:?alias}; pk=${2:?pubkey}; ip=$(ip_of "$a"); [ -n "$ip" ] || die "alias « $a » absent de $IPMAP" | |
| 48 | + printf "[Peer]\n# %s\nPublicKey = %s\nAllowedIPs = %s/32\n" "$a" "$pk" "$ip" > "$PEERS/$a.conf" | |
| 49 | + wg_sync; echo "pair $a → $ip ajouté" ;; | |
| 50 | + rm) a=${1:?alias}; rm -f "$PEERS/$a.conf"; wg_sync; echo "pair $a retiré" ;; | |
| 51 | + ls) | |
| 52 | + wg show "$WG_IF" dump | tail -n +2 | while IFS=$'\t' read -r pk psk ep allowed hs rx tx ka; do | |
| 53 | + ip=${allowed%/32}; a=$(alias_of "$ip"); age="jamais"; [ "$hs" != 0 ] && age="$(( $(date +%s) - hs )) s" | |
| 54 | + printf "%-8s %-12s %-22s handshake %-10s rx %6.1f Mo tx %6.1f Mo\n" "${a:-?}" "$ip" "${ep:-—}" "$age" "$(echo "$rx/1048576" | bc -l)" "$(echo "$tx/1048576" | bc -l)" | |
| 55 | + done ;; | |
| 56 | + *) die "peer add|rm|ls" ;; | |
| 57 | + esac ;; | |
| 58 | + add) | |
| 59 | + d=${1:?domaine}; shift; ups=(); tls=1; ws=0 | |
| 60 | + for x in "$@"; do case "$x" in --no-tls) tls=0;; --websocket) ws=1;; *) ups+=("$(resolve_upstream "$x")");; esac; done | |
| 61 | + [ ${#ups[@]} -gt 0 ] || die "au moins un upstream" | |
| 62 | + { | |
| 63 | + [ $tls = 1 ] && echo "$d {" || echo "http://$d {" | |
| 64 | + echo " import maclustr_errors" # page MacLustr « service indisponible » (502/503/504) avec contact@spboucher.ai | |
| 65 | + echo " encode zstd gzip" | |
| 66 | + echo " log" # journal d accès → journald (journalctl -u caddy), filtrable par request.host | |
| 67 | + echo " reverse_proxy ${ups[*]} {" | |
| 68 | + if [ ${#ups[@]} -gt 1 ]; then | |
| 69 | + echo " lb_policy first"; echo " lb_try_duration 5s" | |
| 70 | + echo " health_uri /"; echo " health_interval 10s"; echo " health_timeout 4s"; echo " health_status 2xx 3xx 4xx" | |
| 71 | + fi | |
| 72 | + echo " header_up X-Forwarded-Proto {scheme}"; echo " header_up X-Real-IP {remote_host}" | |
| 73 | + echo " transport http {"; echo " dial_timeout 5s"; echo " response_header_timeout 300s"; echo " }" | |
| 74 | + echo " }" | |
| 75 | + echo "}" | |
| 76 | + } > "$SITES/$d.caddy" | |
| 77 | + if caddy_reload; then echo "route https://$d → ${ups[*]} active (TLS Let's Encrypt automatique si le DNS de $d pointe vers $PUBLIC_IP)"; else rm -f "$SITES/$d.caddy"; caddy_reload || true; die "Caddyfile invalide, route annulée"; fi ;; | |
| 78 | + redirect) # tunnelctl redirect <domaine-source> <domaine-cible> (ex. apex → www), 308 permanent, TLS auto | |
| 79 | + s=${1:?source}; t=${2:?cible} | |
| 80 | + printf "%s {\n\tredir https://%s{uri} permanent\n}\n" "$s" "$t" > "$SITES/$s.caddy" | |
| 81 | + if caddy_reload; then echo "redirection https://$s → https://$t active"; else rm -f "$SITES/$s.caddy"; caddy_reload || true; die "Caddyfile invalide, redirection annulée"; fi ;; | |
| 82 | + rm) d=${1:?domaine}; rm -f "$SITES/$d.caddy"; caddy_reload; echo "route $d retirée" ;; | |
| 83 | + ls) | |
| 84 | + for f in "$SITES"/*.caddy; do [ -f "$f" ] || continue; d=$(basename "$f" .caddy); up=$(awk '/reverse_proxy/{ $1=""; sub(/ *\{ *$/,""); print }' "$f" | sed 's/^ *//'); printf "%-36s → %s\n" "$d" "$up"; done ;; | |
| 85 | + json) # état machine-lisible (consommé par maclustr-agentd → apps MacLustr) | |
| 86 | + GW_NAME=${GW_NAME:-$(hostname -s | tr a-z A-Z)} | |
| 87 | + wg show "$WG_IF" dump 2>/dev/null | tail -n +2 > /tmp/.wgdump.$$ || true | |
| 88 | + python3 - "$GW_NAME" "$PUBLIC_IP" "$IPMAP" "$SITES" "/tmp/.wgdump.$$" "$(wg show $WG_IF listen-port 2>/dev/null)" "$(systemctl is-active caddy 2>/dev/null)" <<'PY' | |
| 89 | +import sys, json, glob, os, re, time | |
| 90 | +name, pub_ip, ipmap, sites, dump, port, caddy = sys.argv[1:8] | |
| 91 | +alias_of = {} | |
| 92 | +for line in open(ipmap): | |
| 93 | + p = line.split() | |
| 94 | + if len(p) >= 2 and not line.startswith('#'): alias_of[p[1]] = p[0] | |
| 95 | +peers = [] | |
| 96 | +now = int(time.time()) | |
| 97 | +for line in open(dump): | |
| 98 | + f = line.rstrip('\n').split('\t') | |
| 99 | + if len(f) < 8: continue | |
| 100 | + ip = f[3].replace('/32', '') | |
| 101 | + hs = int(f[4] or 0) | |
| 102 | + peers.append({"alias": alias_of.get(ip, "?"), "ip": ip, "endpoint": f[2] if f[2] != '(none)' else None, | |
| 103 | + "handshakeS": (now - hs) if hs else None, "rxBytes": int(f[5] or 0), "txBytes": int(f[6] or 0)}) | |
| 104 | +routes = [] | |
| 105 | +for fn in sorted(glob.glob(os.path.join(sites, '*.caddy'))): | |
| 106 | + dom = os.path.basename(fn)[:-6]; txt = open(fn).read() | |
| 107 | + m = re.search(r'redir\s+(\S+)', txt) | |
| 108 | + if m: | |
| 109 | + routes.append({"domain": dom, "kind": "redirect", "target": m.group(1).replace('{uri}', ''), "upstreams": []}) | |
| 110 | + continue | |
| 111 | + m = re.search(r'reverse_proxy\s+([^{\n]+)', txt) | |
| 112 | + ups = m.group(1).split() if m else [] | |
| 113 | + routes.append({"domain": dom, "kind": "proxy", "upstreams": [{"addr": u, "alias": alias_of.get(u.split(':')[0], None)} for u in ups]}) | |
| 114 | +print(json.dumps({"gateway": name, "ip": pub_ip, "ts": now, | |
| 115 | + "wg": {"listenPort": int(port or 0), "peers": peers}, | |
| 116 | + "caddy": {"active": caddy == "active", "routes": routes}}, ensure_ascii=False)) | |
| 117 | +PY | |
| 118 | + rm -f /tmp/.wgdump.$$ ;; | |
| 119 | + status) | |
| 120 | + echo "== WireGuard $WG_IF ($(wg show $WG_IF listen-port 2>/dev/null) udp) — $(ls "$PEERS"/*.conf 2>/dev/null | wc -l) pairs"; "$0" peer ls | |
| 121 | + echo; echo "== Caddy : $(systemctl is-active caddy) — $(ls "$SITES"/*.caddy 2>/dev/null | wc -l) routes"; "$0" ls ;; | |
| 122 | + *) sed -n '2,12p' "$0"; exit 1 ;; | |
| 123 | +esac | |
added
maclustr-tunnel/wg-forward-setup.sh
+34 −0
@@ -0,0 +1,34 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +# wg-forward-setup.sh — expose sur l'IP WireGuard du Mac des services qui n'écoutent que sur 127.0.0.1 | |
| 3 | +# (ex. spbgit :7420, api-ka :8000) sans toucher aux apps : un relais socat par port, en LaunchDaemon. | |
| 4 | +# Usage : bash wg-forward-setup.sh <ip-wg> <mot-de-passe-sudo> <port> [<port>…] | |
| 5 | +set -euo pipefail | |
| 6 | +WGIP=${1:?ip wg}; PW=${2:?sudo}; shift 2; PORTS=("$@"); [ ${#PORTS[@]} -gt 0 ] || { echo "ports ?"; exit 1; } | |
| 7 | +sudo_(){ echo "$PW" | sudo -S -p '' "$@"; } | |
| 8 | +export PATH=/opt/homebrew/bin:/opt/homebrew/sbin:$PATH | |
| 9 | +[ -x /opt/homebrew/bin/socat ] || HOMEBREW_NO_AUTO_UPDATE=1 brew install -q socat >/dev/null 2>&1 || HOMEBREW_NO_AUTO_UPDATE=1 brew install socat | |
| 10 | +CONF=/opt/homebrew/etc/maclustr-wgfwd.conf | |
| 11 | +T=$(mktemp); { [ -f "$CONF" ] && cat "$CONF"; for p in "${PORTS[@]}"; do echo "$WGIP $p"; done; } | sort -u > "$T" | |
| 12 | +sudo_ install -m 644 -o root "$T" "$CONF"; rm -f "$T" | |
| 13 | +TMPP=$(mktemp) | |
| 14 | +cat > "$TMPP" <<'PLIST' | |
| 15 | +<?xml version="1.0" encoding="UTF-8"?> | |
| 16 | +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
| 17 | +<plist version="1.0"><dict> | |
| 18 | + <key>Label</key><string>io.maclustr.wgfwd</string> | |
| 19 | + <key>ProgramArguments</key><array> | |
| 20 | + <string>/bin/bash</string><string>-c</string> | |
| 21 | + <string>export PATH=/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin; pids=""; while read -r ip port; do [ -z "$ip" ] && continue; ( while true; do /opt/homebrew/bin/socat -T 3600 TCP-LISTEN:$port,bind=$ip,fork,reuseaddr TCP:127.0.0.1:$port 2>>/var/log/maclustr-wgfwd.log; sleep 5; done ) & pids="$pids $!"; done < /opt/homebrew/etc/maclustr-wgfwd.conf; wait</string> | |
| 22 | + </array> | |
| 23 | + <key>RunAtLoad</key><true/> | |
| 24 | + <key>KeepAlive</key><true/> | |
| 25 | + <key>StandardErrorPath</key><string>/var/log/maclustr-wgfwd.log</string> | |
| 26 | +</dict></plist> | |
| 27 | +PLIST | |
| 28 | +sudo_ install -m 644 -o root -g wheel "$TMPP" /Library/LaunchDaemons/io.maclustr.wgfwd.plist; rm -f "$TMPP" | |
| 29 | +sudo_ launchctl bootout system/io.maclustr.wgfwd >/dev/null 2>&1 || true | |
| 30 | +sleep 2 | |
| 31 | +ok=0; for i in 1 2 3 4 5 6; do sudo_ launchctl bootstrap system /Library/LaunchDaemons/io.maclustr.wgfwd.plist 2>/tmp/wgfwd.err && { ok=1; break; }; grep -q -i already /tmp/wgfwd.err && { ok=1; break; }; sleep 3; done | |
| 32 | +[ $ok = 1 ] || { cat /tmp/wgfwd.err; exit 1; } | |
| 33 | +sleep 2 | |
| 34 | +for p in "${PORTS[@]}"; do if sudo_ lsof -nP -iTCP@$WGIP:$p -sTCP:LISTEN >/dev/null 2>&1; then echo "relais $WGIP:$p → 127.0.0.1:$p OK"; else echo "relais $WGIP:$p KO"; fi; done | |
added
maclustr-tunnel/wg-node-setup.sh
+70 −0
@@ -0,0 +1,70 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +# wg-node-setup.sh — installe WireGuard (wireguard-go + wg-quick) sur un Mac du cluster et le raccorde au hub R9128. | |
| 3 | +# Usage : bash wg-node-setup.sh <ip-wg du nœud> <pubkey-serveur> <mot-de-passe-sudo> [iface=$IF] [endpoint=51.255.75.61:51820] | |
| 4 | +# $IF = hub R9128 (France, 10.66.0.x) ; wg1 = hub BHS64 (Québec, 10.67.0.x). Un LaunchDaemon par interface. | |
| 5 | +# Imprime « PUBKEY <clé> » à la fin (à donner à tunnelctl peer add sur la passerelle). | |
| 6 | +set -euo pipefail | |
| 7 | +ADDR=${1:?ip wg}; SERVER_PUB=${2:?pubkey serveur}; PW=${3:?sudo}; IF=${4:-wg0}; ENDPOINT=${5:-51.255.75.61:51820} | |
| 8 | +SUBNET=${ADDR%.*} # ex. 10.67.0 | |
| 9 | +BREW=/opt/homebrew/bin/brew | |
| 10 | +# NB : sudo -S lit le mot de passe sur stdin → ne jamais combiner sudo_ avec un heredoc/pipe de contenu. | |
| 11 | +sudo_(){ echo "$PW" | sudo -S -p '' "$@"; } | |
| 12 | +export PATH=/opt/homebrew/bin:/opt/homebrew/sbin:$PATH | |
| 13 | +[ -x "$BREW" ] || { echo "Homebrew absent"; exit 2; } | |
| 14 | +if ! [ -x /opt/homebrew/bin/wg-quick ]; then HOMEBREW_NO_AUTO_UPDATE=1 "$BREW" install -q wireguard-tools >/dev/null 2>&1 || HOMEBREW_NO_AUTO_UPDATE=1 "$BREW" install wireguard-tools; fi | |
| 15 | +[ -x /opt/homebrew/bin/wireguard-go ] || HOMEBREW_NO_AUTO_UPDATE=1 "$BREW" install -q wireguard-go >/dev/null 2>&1 || true | |
| 16 | +ETC=/opt/homebrew/etc/wireguard | |
| 17 | +sudo_ mkdir -p "$ETC" | |
| 18 | +if ! sudo_ test -f "$ETC/$IF.key"; then | |
| 19 | + K=$(/opt/homebrew/bin/wg genkey); T=$(mktemp); chmod 600 "$T"; printf '%s\n' "$K" > "$T" | |
| 20 | + sudo_ install -m 600 -o root "$T" "$ETC/$IF.key"; rm -f "$T" | |
| 21 | +fi | |
| 22 | +PRIV=$(sudo_ cat "$ETC/$IF.key") | |
| 23 | +PUB=$(printf '%s' "$PRIV" | /opt/homebrew/bin/wg pubkey) | |
| 24 | +TMPC=$(mktemp); chmod 600 "$TMPC" | |
| 25 | +{ | |
| 26 | + echo "[Interface]" | |
| 27 | + echo "PrivateKey = $PRIV" | |
| 28 | + echo "Address = $ADDR/24" | |
| 29 | + echo "MTU = 1380" | |
| 30 | + echo | |
| 31 | + echo "[Peer]" | |
| 32 | + echo "# hub R9128 (OVH)" | |
| 33 | + echo "PublicKey = $SERVER_PUB" | |
| 34 | + echo "Endpoint = $ENDPOINT" | |
| 35 | + echo "AllowedIPs = $SUBNET.0/24" | |
| 36 | + echo "PersistentKeepalive = 25" | |
| 37 | +} > "$TMPC" | |
| 38 | +sudo_ install -m 600 -o root "$TMPC" "$ETC/$IF.conf"; rm -f "$TMPC" | |
| 39 | + | |
| 40 | +# LaunchDaemon : monte $IF au boot (wireguard-go reste en arrière-plan) et le remonte s'il tombe. | |
| 41 | +TMPP=$(mktemp) | |
| 42 | +cat > "$TMPP" <<PLIST | |
| 43 | +<?xml version="1.0" encoding="UTF-8"?> | |
| 44 | +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
| 45 | +<plist version="1.0"><dict> | |
| 46 | + <key>Label</key><string>io.maclustr.wireguard-$IF</string> | |
| 47 | + <key>ProgramArguments</key><array> | |
| 48 | + <string>/bin/bash</string><string>-c</string> | |
| 49 | + <string>export PATH=/opt/homebrew/bin:/opt/homebrew/sbin:/usr/bin:/bin:/usr/sbin:/sbin; up(){ n=\$(cat /var/run/wireguard/$IF.name 2>/dev/null) && [ -n "\$n" ] && /opt/homebrew/bin/wg show "\$n" >/dev/null 2>&1; }; while true; do if ! up; then /opt/homebrew/bin/wg-quick down $IF >/dev/null 2>&1; /opt/homebrew/bin/wg-quick up $IF || true; fi; sleep 30; done</string> | |
| 50 | + </array> | |
| 51 | + <key>RunAtLoad</key><true/> | |
| 52 | + <key>KeepAlive</key><true/> | |
| 53 | + <key>StandardOutPath</key><string>/var/log/maclustr-wireguard-$IF.log</string> | |
| 54 | + <key>StandardErrorPath</key><string>/var/log/maclustr-wireguard-$IF.log</string> | |
| 55 | +</dict></plist> | |
| 56 | +PLIST | |
| 57 | +sudo_ install -m 644 -o root -g wheel "$TMPP" /Library/LaunchDaemons/io.maclustr.wireguard-$IF.plist; rm -f "$TMPP" | |
| 58 | +sudo_ launchctl bootout system/io.maclustr.wireguard-$IF >/dev/null 2>&1 || true | |
| 59 | +sudo_ /opt/homebrew/bin/wg-quick down $IF >/dev/null 2>&1 || true | |
| 60 | +sleep 2 | |
| 61 | +ok=0; for i in 1 2 3 4 5 6; do | |
| 62 | + if sudo_ launchctl bootstrap system /Library/LaunchDaemons/io.maclustr.wireguard-$IF.plist 2>/tmp/wg-bootstrap.err; then ok=1; break; fi | |
| 63 | + grep -q -i "already" /tmp/wg-bootstrap.err && { ok=1; break; } | |
| 64 | + sleep 3 | |
| 65 | +done | |
| 66 | +[ $ok = 1 ] || { cat /tmp/wg-bootstrap.err; echo "bootstrap KO → démarrage direct"; sudo_ /opt/homebrew/bin/wg-quick up $IF; } | |
| 67 | +# NB : `wg show $IF` ne résout pas le nom sur macOS (wireguard-tools 1.0.2026) → passer par /var/run/wireguard/$IF.name. | |
| 68 | +for i in $(seq 1 30); do N=$(sudo_ cat /var/run/wireguard/$IF.name 2>/dev/null || true); [ -n "$N" ] && sudo_ /opt/homebrew/bin/wg show "$N" >/dev/null 2>&1 && break; sleep 1; done | |
| 69 | +sudo_ /opt/homebrew/bin/wg show "$N" | head -3 | |
| 70 | +echo "NODE $(cat ~/.maclustr-node 2>/dev/null) IF $N PUBKEY $PUB" | |
added
mld
+20 −0
@@ -0,0 +1,20 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +# mld (laptop) — relaie la commande à la passerelle M1M32 puis rapatrie le registre. | |
| 3 | +# mld plan | mld deploy <app> | mld move <app> --to X | mld status --live | mld stage <dir> <app> ... | |
| 4 | +# `mld stage <dir-local> <app>` : pousse un dossier du laptop dans le staging de la passerelle (~/dispatch/stage/<app>/dir/). | |
| 5 | +set -u | |
| 6 | +GW=M1M32 | |
| 7 | +SKILL_DIR="$(cd "$(dirname "$0")" && pwd)" | |
| 8 | +if [ "${1:-}" = "stage" ]; then | |
| 9 | + src="${2:?usage: mld stage <dir-local> <app>}"; app="${3:?usage: mld stage <dir-local> <app>}" | |
| 10 | + # Un fichier .mldignore à la racine du projet ajoute des exclusions (syntaxe rsync) — p. ex. data/ avec des profils navigateur. | |
| 11 | + extra=(); [ -f "${src%/}/.mldignore" ] && extra=(--exclude-from="${src%/}/.mldignore") | |
| 12 | + rsync -az --delete --exclude .git --exclude node_modules --exclude .next --exclude .venv --exclude .DS_Store ${extra[@]+"${extra[@]}"} \ | |
| 13 | + --rsync-path="mkdir -p dispatch/stage/$app/dir && rsync" "${src%/}/" "$GW:dispatch/stage/$app/dir/" && echo "OK stage $app → $GW:~/dispatch/stage/$app/dir/${extra:+ (.mldignore appliqué)}" | |
| 14 | + exit $? | |
| 15 | +fi | |
| 16 | +ssh -t -o LogLevel=ERROR "$GW" "~/maclustr-dispatch/bin/mld $(printf '%q ' "$@")" | |
| 17 | +rc=$? | |
| 18 | +# registre : copie locale (source de vérité = passerelle) | |
| 19 | +scp -q "$GW:dispatch/registry.json" "$SKILL_DIR/cluster-registry.json" 2>/dev/null | |
| 20 | +exit $rc | |
added
mlmobile
+5 −0
@@ -0,0 +1,5 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +# mlmobile (laptop) — client du coordinateur des iPhones du cluster (maclustr-mobile sur M4M64a:9320, via Tailscale). | |
| 3 | +# mlmobile nodes | ping | info | fetch URL | run script.js | map script.js items.json | fetch-list urls.txt | jobs | job ID | |
| 4 | +export MLMOBILE_URL="${MLMOBILE_URL:-http://M4M64a.maclustr.io:9320}" | |
| 5 | +exec /usr/bin/env python3 "$HOME/Desktop/Cluster/maclustr-mobile/bin/mlmobile" "$@" | |
added
mlt
+43 −0
@@ -0,0 +1,43 @@ | ||
| 1 | +#!/usr/bin/env bash | |
| 2 | +# mlt — MacLustr Tunnel depuis le laptop : le « ngrok privé » du cluster (WireGuard + Caddy sur une passerelle OVH). | |
| 3 | +# | |
| 4 | +# mlt [-g BHS64] status pairs WireGuard + routes publiques de la passerelle | |
| 5 | +# mlt [-g …] peer <alias> [<alias>…] installe WireGuard sur un Mac (brew wireguard-tools, LaunchDaemon) et l'enregistre sur le hub | |
| 6 | +# mlt [-g …] add <domaine> <alias>:<port> [<alias>:<port>…] expose https://<domaine> → nœud(s) (TLS auto ; DNS A → IP de la passerelle) | |
| 7 | +# mlt [-g …] redirect <domaine> <cible> ex. mlt redirect qc26.ca www.qc26.ca | |
| 8 | +# mlt [-g …] rm <domaine> | ls | ssh | |
| 9 | +# | |
| 10 | +# Passerelle : BHS64 (Beauharnois, wg1 / 10.67.0.x, IP 51.161.112.61). R9128 (Gravelines, wg0 / 10.66.0.x) retirée le 2026-10-02. | |
| 11 | +# Équivalent de `ngrok http <port> --url=<domaine>` : mlt add <domaine> <nœud>:<port> (+ A → IP passerelle chez GoDaddy). | |
| 12 | +set -euo pipefail | |
| 13 | +HERE="$(cd "$(dirname "$0")" && pwd)" | |
| 14 | +TOOLS="$HERE/maclustr-tunnel" | |
| 15 | +GW=${MLT_GW:-BHS64} | |
| 16 | +if [ "${1:-}" = "-g" ]; then GW=$2; shift 2; fi | |
| 17 | +case "$GW" in | |
| 18 | + BHS64) GW_IP=51.161.112.61; IFACE=wg1; SUBNET=10.67.0; SERVER_PUB=${MLT_PUB_BHS64:-$(cat "$TOOLS/hub-BHS64.pub" 2>/dev/null || true)} ;; | |
| 19 | + *) echo "mlt: passerelle inconnue $GW (BHS64 seulement depuis le 2026-10-02)" >&2; exit 1 ;; | |
| 20 | +esac | |
| 21 | +die(){ echo "mlt: $*" >&2; exit 1; } | |
| 22 | +sudo_pw_for(){ local pw; pw=$(ssh M1M32 "cat ~/dispatch/.sudo-$1 2>/dev/null"); echo "${pw:-492592}"; } # loués : M1M32:~/dispatch/.sudo-<alias> ; LAN : 492592 | |
| 23 | +cmd=${1:-status}; shift || true | |
| 24 | +case "$cmd" in | |
| 25 | + status|ls) ssh "$GW" "sudo tunnelctl $cmd" ;; | |
| 26 | + add|redirect|rm) ssh "$GW" "sudo tunnelctl $cmd $(printf '%q ' "$@")" ;; | |
| 27 | + ssh) exec ssh "$GW" ;; | |
| 28 | + peer) | |
| 29 | + [ $# -ge 1 ] || die "mlt peer <alias>…" | |
| 30 | + [ -n "$SERVER_PUB" ] || die "clé publique du hub $GW inconnue (fichier $TOOLS/hub-$GW.pub)" | |
| 31 | + for a in "$@"; do | |
| 32 | + ip=$(awk -v a="$a" '$1==a{print $2}' "$TOOLS/ipmap" | sed "s/^10\.66\.0\./$SUBNET./"); [ -n "$ip" ] || die "alias $a absent de $TOOLS/ipmap" | |
| 33 | + pw=$(sudo_pw_for "$a") | |
| 34 | + echo "== $a → $ip ($IFACE → $GW $GW_IP)" | |
| 35 | + scp -q "$TOOLS/wg-node-setup.sh" "$a:/tmp/wg-node-setup.sh" | |
| 36 | + out=$(ssh "$a" "bash /tmp/wg-node-setup.sh $ip $SERVER_PUB '$pw' $IFACE $GW_IP:51820 2>&1" | tail -3) | |
| 37 | + echo "$out" | grep -v PUBKEY | sed 's/^/ /' | |
| 38 | + pub=$(echo "$out" | awk '/PUBKEY/{print $NF}'); [ -n "$pub" ] || die "pas de clé publique renvoyée par $a" | |
| 39 | + ssh "$GW" "sudo tunnelctl peer add $a $pub" | sed 's/^/ /' | |
| 40 | + sleep 3; ssh "$GW" "ping -c 1 -W 3 -q $ip >/dev/null && echo ' ping OK depuis la passerelle' || echo ' ping KO (attendre le handshake, keepalive 25 s)'" | |
| 41 | + done ;; | |
| 42 | + *) sed -n '2,12p' "$0"; exit 1 ;; | |
| 43 | +esac | |
added
ovh-manager/README.md
+24 −0
@@ -0,0 +1,24 @@ | ||
| 1 | +# Installation d'un serveur dédié OVH via le manager piloté en Safari (JXA) | |
| 2 | + | |
| 3 | +Prérequis : Safari ouvert et connecté à https://manager.ca.ovhcloud.com ; Safari → Développement → « Autoriser JavaScript des événements Apple ». | |
| 4 | +Le contenu du serveur vit dans un `iframe` de même origine : tous les scripts passent par `document.querySelector('iframe').contentDocument`. | |
| 5 | + | |
| 6 | +```bash | |
| 7 | +R=~/Desktop/cluster-skill/ovh-manager | |
| 8 | +osascript -e 'tell application "Safari" to set URL of front document to "https://manager.ca.ovhcloud.com/#/dedicated/server/<nom-ovh>"'; sleep 15 | |
| 9 | +$R/runjs.sh $R/inspect.js # fiche (modèle, CPU, RAM, ID, baie) | $R/runjs.sh $R/net.js → IPv4/IPv6 | |
| 10 | +$R/runjs.sh $R/install.js # bouton « Installer mon serveur » | |
| 11 | +$R/runjs.sh $R/modal.js # liste des OS | |
| 12 | +$R/runjs.sh $R/os.js # coche « Ubuntu Server 24.04 » | |
| 13 | +$R/runjs.sh $R/next.js # « Suivant » → étape 4 directement | |
| 14 | +# adapter fill-<alias>.js (hostname + clé + postinstall.sh) — généré en Python pour l'échappement JSON, voir fill-bhs128.js | |
| 15 | +$R/runjs.sh $R/fill-<alias>.js && $R/runjs.sh $R/verify.js | |
| 16 | +$R/runjs.sh $R/valider.js # lance l'installation | |
| 17 | +$R/runjs.sh $R/prog.js # « Running Hardware Reboot ( 3 / 21 ) » … ~20 min | |
| 18 | +$R/wait-<alias>.sh # boucle : progression + ssh ubuntu@<ip> jusqu'au marqueur /root/maclustr-postinstall.done | |
| 19 | +``` | |
| 20 | + | |
| 21 | +Post-install commun (`postinstall.sh`) : clés maclustr-cluster / maclustr-m1m32 / maclustr-agentd sur ubuntu et root, apt de base (git, python3, htop, tmux, jq, rsync, curl, build-essential, unattended-upgrades, wireguard, ufw, bc), TZ America/Toronto, marqueur. | |
| 22 | +Ensuite : alias `~/.ssh/config` (laptop + M1M32), `NODES.md`, `~/.claude/CLAUDE.md`. | |
| 23 | + | |
| 24 | +Historique : R9128 (2026-09-08), BHS64 + BHS64b (2026-09-10), BHS128 (2026-09-10 soir), BHS128b (2026-09-14 — ns5035327, 51.161.112.85, ADVANCE-2 EPYC 4344P mais 128 Go : installé sous le hostname `bhs64c` puis renommé `bhs128b` après `free -g` ; `fill-bhs128b.js`/`wait-bhs128b.sh`). Leçon : la fiche du manager ne donne pas la RAM — nommer après le premier `free -g`. | |
added
ovh-manager/fill-bhs128.js
+10 −0
@@ -0,0 +1,10 @@ | ||
| 1 | +(function(){ | |
| 2 | + const d = document.querySelector('iframe').contentDocument; | |
| 3 | + function setv(sel, v){ const e=d.querySelector(sel); if(!e) return sel+' introuvable'; | |
| 4 | + e.focus(); e.value=v; ['input','change','blur'].forEach(t=>e.dispatchEvent(new Event(t,{bubbles:true}))); return sel+'='+e.value.length; } | |
| 5 | + const r=[]; | |
| 6 | + r.push(setv('input[name=hostname]', 'bhs128')); | |
| 7 | + r.push(setv('textarea[name=sshKey]', "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG5oW8BH99QOZT/iw1z7kFfDWhLaHgOqi5RSvjMfzGBk maclustr-cluster")); | |
| 8 | + r.push(setv('textarea[name=postInstallationScript]', "#!/bin/bash\n# maclustr post-install (OVH BHS128, 2026-09-10) : cl\u00e9s du cluster pour ubuntu + root, paquets de base, marqueur.\nset -e\nfor u in ubuntu root; do\n h=$(getent passwd $u | cut -d: -f6); mkdir -p \"$h/.ssh\"; chmod 700 \"$h/.ssh\"\n for k in \"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG5oW8BH99QOZT/iw1z7kFfDWhLaHgOqi5RSvjMfzGBk maclustr-cluster\" \"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIpKd9RkY8MFNjXKd0wzLA12YE9gVNItBUe9C43JXrTk maclustr-m1m32\" \"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDzM4+zbf9lRoXfp7imfaqSff0mdCei7aDO1aRXNpxpX maclustr-agentd\"; do grep -qF \"$k\" \"$h/.ssh/authorized_keys\" 2>/dev/null || echo \"$k\" >> \"$h/.ssh/authorized_keys\"; done\n chmod 600 \"$h/.ssh/authorized_keys\"; chown -R $u \"$h/.ssh\"\ndone\nexport DEBIAN_FRONTEND=noninteractive\napt-get update -qq && apt-get install -y -qq git python3 htop tmux jq rsync curl build-essential unattended-upgrades wireguard wireguard-tools ufw bc >/dev/null 2>&1 || true\ntimedatectl set-timezone America/Toronto || true\ndate > /root/maclustr-postinstall.done\n")); | |
| 9 | + return r.join(' | '); | |
| 10 | +})() | |
| \ No newline at end of file | ||
added
ovh-manager/fill-bhs128b.js
+10 −0
@@ -0,0 +1,10 @@ | ||
| 1 | +(function(){ | |
| 2 | + const d = document.querySelector('iframe').contentDocument; | |
| 3 | + function setv(sel, v){ const e=d.querySelector(sel); if(!e) return sel+' introuvable'; | |
| 4 | + e.focus(); e.value=v; ['input','change','blur'].forEach(t=>e.dispatchEvent(new Event(t,{bubbles:true}))); return sel+'='+e.value.length; } | |
| 5 | + const r=[]; | |
| 6 | + r.push(setv('input[name=hostname]', 'bhs64c')); | |
| 7 | + r.push(setv('textarea[name=sshKey]', "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG5oW8BH99QOZT/iw1z7kFfDWhLaHgOqi5RSvjMfzGBk maclustr-cluster")); | |
| 8 | + r.push(setv('textarea[name=postInstallationScript]', "#!/bin/bash\n# maclustr post-install (OVH BHS64c, 2026-09-14) : cl\u00e9s du cluster pour ubuntu + root, paquets de base, marqueur.\nset -e\nfor u in ubuntu root; do\n h=$(getent passwd $u | cut -d: -f6); mkdir -p \"$h/.ssh\"; chmod 700 \"$h/.ssh\"\n for k in \"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG5oW8BH99QOZT/iw1z7kFfDWhLaHgOqi5RSvjMfzGBk maclustr-cluster\" \"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIpKd9RkY8MFNjXKd0wzLA12YE9gVNItBUe9C43JXrTk maclustr-m1m32\" \"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDzM4+zbf9lRoXfp7imfaqSff0mdCei7aDO1aRXNpxpX maclustr-agentd\"; do grep -qF \"$k\" \"$h/.ssh/authorized_keys\" 2>/dev/null || echo \"$k\" >> \"$h/.ssh/authorized_keys\"; done\n chmod 600 \"$h/.ssh/authorized_keys\"; chown -R $u \"$h/.ssh\"\ndone\nexport DEBIAN_FRONTEND=noninteractive\napt-get update -qq && apt-get install -y -qq git python3 htop tmux jq rsync curl build-essential unattended-upgrades wireguard wireguard-tools ufw bc >/dev/null 2>&1 || true\ntimedatectl set-timezone America/Toronto || true\ndate > /root/maclustr-postinstall.done\n")); | |
| 9 | + return r.join(' | '); | |
| 10 | +})() | |
| \ No newline at end of file | ||
added
ovh-manager/inspect.js
+13 −0
@@ -0,0 +1,13 @@ | ||
| 1 | +(function(){ | |
| 2 | + const out = {url: location.href, title: document.title}; | |
| 3 | + const fr = document.querySelector('iframe'); | |
| 4 | + out.iframe = fr ? fr.src : null; | |
| 5 | + try { | |
| 6 | + const d = fr.contentDocument; | |
| 7 | + out.iframeReady = d.readyState; | |
| 8 | + out.h = [...d.querySelectorAll('h1,h2,h3')].map(e=>e.textContent.trim()).filter(Boolean).slice(0,15); | |
| 9 | + out.buttons = [...d.querySelectorAll('button, a.oui-button, a[role=button]')].map(e=>e.textContent.trim()).filter(Boolean).slice(0,60); | |
| 10 | + out.text = d.body.innerText.slice(0,1500); | |
| 11 | + } catch(e){ out.err = String(e); } | |
| 12 | + return JSON.stringify(out); | |
| 13 | +})() | |
added
ovh-manager/install.js
+6 −0
@@ -0,0 +1,6 @@ | ||
| 1 | +(function(){ | |
| 2 | + const d = document.querySelector('iframe').contentDocument; | |
| 3 | + const b = [...d.querySelectorAll('button, a')].find(e=>e.textContent.trim()==='Installer mon serveur'); | |
| 4 | + if(!b) return 'bouton introuvable'; | |
| 5 | + b.click(); return 'clicked'; | |
| 6 | +})() | |
added
ovh-manager/modal.js
+7 −0
@@ -0,0 +1,7 @@ | ||
| 1 | +(function(){ | |
| 2 | + const d = document.querySelector('iframe').contentDocument; | |
| 3 | + const m = d.querySelector('.oui-modal, .modal, [role=dialog]') || d.body; | |
| 4 | + const labels = [...m.querySelectorAll('label')].map(e=>e.textContent.trim().replace(/\s+/g,' ')).filter(Boolean).slice(0,80); | |
| 5 | + const buttons = [...m.querySelectorAll('button')].map(e=>e.textContent.trim()).filter(Boolean); | |
| 6 | + return JSON.stringify({url: location.href, text: m.innerText.slice(0,1200), labels, buttons}); | |
| 7 | +})() | |
added
ovh-manager/net.js
+10 −0
@@ -0,0 +1,10 @@ | ||
| 1 | +(function(){ | |
| 2 | + const d = document.querySelector('iframe').contentDocument; | |
| 3 | + const t = d.body.innerText; | |
| 4 | + const i = t.indexOf('Réseau\nIPv4'); | |
| 5 | + const m = t.match(/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g); | |
| 6 | + const v6 = t.match(/[0-9a-f:]+::\/\d+/gi); | |
| 7 | + // essai via les éléments copiables | |
| 8 | + const copies = [...d.querySelectorAll('[data-clipboard-text], input[readonly], oui-clipboard input, .oui-clipboard__text, span.copy')].map(e=>e.getAttribute('data-clipboard-text')||e.value||e.textContent.trim()).filter(Boolean); | |
| 9 | + return JSON.stringify({ips:m, v6:v6, copies:copies, slice:t.slice(i, i+400)}); | |
| 10 | +})() | |
added
ovh-manager/next.js
+6 −0
@@ -0,0 +1,6 @@ | ||
| 1 | +(function(){ | |
| 2 | + const d = document.querySelector('iframe').contentDocument; | |
| 3 | + const bs = [...d.querySelectorAll('button')].filter(e=>e.textContent.trim()==='Suivant' && !e.disabled && e.offsetParent!==null); | |
| 4 | + if(!bs.length) return 'aucun Suivant actif'; | |
| 5 | + bs[0].click(); return 'Suivant clicked ('+bs.length+' visibles)'; | |
| 6 | +})() | |
added
ovh-manager/os.js
+8 −0
@@ -0,0 +1,8 @@ | ||
| 1 | +(function(){ | |
| 2 | + const d = document.querySelector('iframe').contentDocument; | |
| 3 | + const l = [...d.querySelectorAll('label')].find(e=>e.textContent.includes('Ubuntu Server 24.04')); | |
| 4 | + if(!l) return 'label introuvable'; | |
| 5 | + l.click(); | |
| 6 | + const inp = l.querySelector('input') || d.getElementById(l.getAttribute('for')); | |
| 7 | + return 'clicked; checked=' + (inp && inp.checked); | |
| 8 | +})() | |
added
ovh-manager/postinstall.sh
+12 −0
@@ -0,0 +1,12 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +# maclustr post-install (OVH BHS128, 2026-09-10) : clés du cluster pour ubuntu + root, paquets de base, marqueur. | |
| 3 | +set -e | |
| 4 | +for u in ubuntu root; do | |
| 5 | + h=$(getent passwd $u | cut -d: -f6); mkdir -p "$h/.ssh"; chmod 700 "$h/.ssh" | |
| 6 | + for k in "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG5oW8BH99QOZT/iw1z7kFfDWhLaHgOqi5RSvjMfzGBk maclustr-cluster" "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIpKd9RkY8MFNjXKd0wzLA12YE9gVNItBUe9C43JXrTk maclustr-m1m32" "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDzM4+zbf9lRoXfp7imfaqSff0mdCei7aDO1aRXNpxpX maclustr-agentd"; do grep -qF "$k" "$h/.ssh/authorized_keys" 2>/dev/null || echo "$k" >> "$h/.ssh/authorized_keys"; done | |
| 7 | + chmod 600 "$h/.ssh/authorized_keys"; chown -R $u "$h/.ssh" | |
| 8 | +done | |
| 9 | +export DEBIAN_FRONTEND=noninteractive | |
| 10 | +apt-get update -qq && apt-get install -y -qq git python3 htop tmux jq rsync curl build-essential unattended-upgrades wireguard wireguard-tools ufw bc >/dev/null 2>&1 || true | |
| 11 | +timedatectl set-timezone America/Toronto || true | |
| 12 | +date > /root/maclustr-postinstall.done | |
added
ovh-manager/prog.js
+7 −0
@@ -0,0 +1,7 @@ | ||
| 1 | +(function(){ | |
| 2 | + const d = document.querySelector('iframe').contentDocument; | |
| 3 | + const m = d.querySelector('.oui-modal, .modal, [role=dialog]'); | |
| 4 | + const t = (m||d.body).innerText; | |
| 5 | + const i = t.indexOf('Progression'); | |
| 6 | + return JSON.stringify({url:location.href, modal:!!m, text:(i>=0? t.slice(i, i+700) : t.slice(0,700))}); | |
| 7 | +})() | |
added
ovh-manager/runjs.sh
+9 −0
@@ -0,0 +1,9 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +# Usage: runjs.sh <file.js> — exécute le JS dans le document Safari au premier plan et affiche le résultat | |
| 3 | +JS=$(cat "$1") | |
| 4 | +osascript -l JavaScript -e ' | |
| 5 | +function run(argv){ | |
| 6 | + const safari = Application("Safari"); | |
| 7 | + const doc = safari.windows[0].currentTab(); | |
| 8 | + return safari.doJavaScript(argv[0], {in: doc}); | |
| 9 | +}' "$JS" | |
added
ovh-manager/step.js
+6 −0
@@ -0,0 +1,6 @@ | ||
| 1 | +(function(){ | |
| 2 | + const d = document.querySelector('iframe').contentDocument; | |
| 3 | + const m = d.querySelector('.oui-modal, .modal, [role=dialog]') || d.body; | |
| 4 | + const fields = [...m.querySelectorAll('input, textarea, select')].filter(e=>e.offsetParent!==null).map(e=>({tag:e.tagName, name:e.name, type:e.type, value:(e.value||'').slice(0,60), placeholder:e.placeholder})); | |
| 5 | + return JSON.stringify({text: m.innerText.slice(0,900), fields}); | |
| 6 | +})() | |
added
ovh-manager/valider.js
+5 −0
@@ -0,0 +1,5 @@ | ||
| 1 | +(function(){ | |
| 2 | + const d = document.querySelector('iframe').contentDocument; | |
| 3 | + const b=[...d.querySelectorAll('button')].find(e=>e.textContent.trim()==='Valider' && e.offsetParent!==null && !e.disabled); | |
| 4 | + if(!b) return 'Valider introuvable'; b.click(); return 'Valider clicked'; | |
| 5 | +})() | |
added
ovh-manager/verify.js
+7 −0
@@ -0,0 +1,7 @@ | ||
| 1 | +(function(){ | |
| 2 | + const d = document.querySelector('iframe').contentDocument; | |
| 3 | + const h=d.querySelector('input[name=hostname]'), k=d.querySelector('textarea[name=sshKey]'), p=d.querySelector('textarea[name=postInstallationScript]'); | |
| 4 | + const errs=[...d.querySelectorAll('.oui-field__error, .error, [class*=error]')].map(e=>e.textContent.trim()).filter(Boolean).slice(0,10); | |
| 5 | + const val=[...d.querySelectorAll('button')].filter(e=>e.textContent.trim()==='Valider' && e.offsetParent!==null).map(e=>'disabled='+e.disabled); | |
| 6 | + return JSON.stringify({hostname:h.value, key:k.value.slice(0,40), scriptLen:p.value.length, scriptHead:p.value.slice(0,60), errs, val}); | |
| 7 | +})() | |
added
ovh-manager/wait-bhs128.sh
+9 −0
@@ -0,0 +1,9 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +# attend la fin de l'install OVH puis que ssh réponde | |
| 3 | +for i in $(seq 1 90); do | |
| 4 | + P=$(/tmp/ovh/runjs.sh /tmp/ovh/prog.js 2>/dev/null | python3 -c 'import sys,json,re; t=json.load(sys.stdin)["text"]; m=re.search(r"\(\s*(\d+)\s*/\s*(\d+)\s*\)", t); print(m.group(0) if m else t[:120].replace("\n"," "))') | |
| 5 | + echo "$(date +%H:%M:%S) $P" | |
| 6 | + if ssh -o ConnectTimeout=5 -o BatchMode=yes -o StrictHostKeyChecking=accept-new ubuntu@51.161.112.69 'hostname && test -f /root/maclustr-postinstall.done 2>/dev/null || sudo test -f /root/maclustr-postinstall.done' 2>/dev/null; then echo READY; exit 0; fi | |
| 7 | + sleep 30 | |
| 8 | +done | |
| 9 | +echo TIMEOUT; exit 1 | |
added
ovh-manager/wait-bhs128b.sh
+9 −0
@@ -0,0 +1,9 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +# attend la fin de l'install OVH puis que ssh réponde | |
| 3 | +for i in $(seq 1 90); do | |
| 4 | + P=$(/Users/simon-pierreboucher/Desktop/cluster-skill/ovh-manager/runjs.sh /Users/simon-pierreboucher/Desktop/cluster-skill/ovh-manager/prog.js 2>/dev/null | python3 -c 'import sys,json,re; t=json.load(sys.stdin)["text"]; m=re.search(r"\(\s*(\d+)\s*/\s*(\d+)\s*\)", t); print(m.group(0) if m else t[:120].replace("\n"," "))') | |
| 5 | + echo "$(date +%H:%M:%S) $P" | |
| 6 | + if ssh -o ConnectTimeout=5 -o BatchMode=yes -o StrictHostKeyChecking=accept-new ubuntu@51.161.112.85 'hostname && test -f /root/maclustr-postinstall.done 2>/dev/null || sudo test -f /root/maclustr-postinstall.done' 2>/dev/null; then echo READY; exit 0; fi | |
| 7 | + sleep 30 | |
| 8 | +done | |
| 9 | +echo TIMEOUT; exit 1 | |
added
release-maclustr-macos.sh
+47 −0
@@ -0,0 +1,47 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +# release-maclustr-macos.sh — chaîne de release de l'app macOS MacLustr (maclustr-v2) depuis le laptop | |
| 3 | +# quand la licence Xcode n'est pas acceptée (swift/xcodebuild/notarytool bloqués) : | |
| 4 | +# 1. build + bundle avec la chaîne Command Line Tools (SDK macOS 26) | |
| 5 | +# 2. signature Developer ID + DMG sur le laptop (codesign/hdiutil ne dépendent pas de la licence) | |
| 6 | +# 3. notarisation + staple sur M4M64a (Xcode 26.6, clé API App Store Connect L379BDKR69) | |
| 7 | +# 4. rapatriement du DMG stapled, installation dans /Applications (optionnel : --install) | |
| 8 | +# Usage : release-maclustr-macos.sh [--install] [--skip-build] | |
| 9 | +set -euo pipefail | |
| 10 | +PROJ="$HOME/Desktop/Cluster/CLUSTER_APPLE/maclustr-v2" | |
| 11 | +SWIFT=/Library/Developer/CommandLineTools/usr/bin/swift | |
| 12 | +SDK=/Library/Developer/CommandLineTools/SDKs/MacOSX26.sdk | |
| 13 | +NODE=M4M64a | |
| 14 | +KEY=~/.appstoreconnect/private_keys/AuthKey_L379BDKR69.p8 | |
| 15 | +KEY_ID=L379BDKR69 | |
| 16 | +ISSUER=013e761f-e05c-4d93-a50b-f3d960adae4a | |
| 17 | +INSTALL=0; SKIP_BUILD=0 | |
| 18 | +for a in "$@"; do case "$a" in --install) INSTALL=1;; --skip-build) SKIP_BUILD=1;; esac; done | |
| 19 | + | |
| 20 | +cd "$PROJ" | |
| 21 | +VERSION=$(grep -E '^VERSION=' build.sh | head -1 | cut -d'"' -f2) | |
| 22 | +echo "=== MacLustr macOS $VERSION ===" | |
| 23 | +if [ "$SKIP_BUILD" = 0 ]; then | |
| 24 | + echo ">> build + bundle (CLT swift, SDK macOS 26)" | |
| 25 | + SWIFT="$SWIFT" SDKROOT="$SDK" ./build.sh app 2>&1 | grep -E "error|Build complete|App bundle" | tail -5 | |
| 26 | +fi | |
| 27 | +echo ">> signature Developer ID" | |
| 28 | +./build.sh sign 2>&1 | grep -E "Signing|valid|error" | tail -3 | |
| 29 | +echo ">> DMG" | |
| 30 | +./build.sh dmg 2>&1 | grep -E "created|error" | tail -2 | |
| 31 | +[ -f MacLustr.dmg ] || { echo "DMG absent"; exit 1; } | |
| 32 | +echo ">> notarisation sur $NODE" | |
| 33 | +scp -q MacLustr.dmg "$NODE:/tmp/MacLustr-$VERSION.dmg" | |
| 34 | +ssh "$NODE" "xcrun notarytool submit /tmp/MacLustr-$VERSION.dmg --key $KEY --key-id $KEY_ID --issuer $ISSUER --wait 2>&1 | grep -E 'id:|status:' | tail -2 && xcrun stapler staple /tmp/MacLustr-$VERSION.dmg 2>&1 | tail -1" | |
| 35 | +scp -q "$NODE:/tmp/MacLustr-$VERSION.dmg" MacLustr.dmg | |
| 36 | +echo ">> staple de l'app locale" | |
| 37 | +# le ticket est aussi valable pour l'app : on l'extrait du DMG notarisé | |
| 38 | +MNT=$(hdiutil attach -nobrowse -readonly MacLustr.dmg | awk -F'\t' '/Volumes/{print $NF}') | |
| 39 | +rm -rf MacLustr.app && cp -R "$MNT/MacLustr.app" MacLustr.app && hdiutil detach "$MNT" -quiet | |
| 40 | +spctl -a -vv MacLustr.app 2>&1 | tail -2 | |
| 41 | +if [ "$INSTALL" = 1 ]; then | |
| 42 | + echo ">> installation dans /Applications" | |
| 43 | + pkill -x MacLustr 2>/dev/null || true | |
| 44 | + rm -rf /Applications/MacLustr.app && cp -R MacLustr.app /Applications/MacLustr.app | |
| 45 | + open /Applications/MacLustr.app && echo "MacLustr $VERSION lancée" | |
| 46 | +fi | |
| 47 | +echo "OK : $PROJ/MacLustr.dmg ($VERSION, notarisé, stapled)" | |
| 48 | ||