# Guide CLI et SDK

## Aperçu

Clore.ai fournit un **API REST** qui permet un accès programmatique complet au marché des GPU — lister les serveurs, créer des commandes, surveiller les déploiements et annuler les locations.

> **Remarque :** Il n'existe pas de binaire CLI officiel pour le moment. Toute automatisation se fait directement via l'API REST en utilisant des outils comme `curl`, Python ou Node.js.

**URL de base :** `https://api.clore.ai/v1`

**Format de réponse :** JSON. Chaque réponse inclut un `code` champ indiquant le statut.

***

## Authentification

Générez votre clé API depuis le [Tableau de bord Clore.ai](https://clore.ai):

1. Connectez-vous à votre compte
2. Accédez à **API** section dans les paramètres
3. Générez et copiez votre clé API

**Format de l'en-tête :**

```
auth: VOTRE_CLE_API
```

> ⚠️ **Important :** L'en-tête auth est `auth`, **pas** `Authorization: Bearer`. L'utilisation du mauvais format retournera le code `3` (Jeton API invalide).

**Exemple :**

```bash
curl -H 'auth: VOTRE_CLE_API' 'https://api.clore.ai/v1/marketplace'
```

***

## Démarrage rapide

### Lister les serveurs du marché

Parcourez tous les serveurs GPU disponibles :

```bash
curl -XGET \
  -H 'auth: VOTRE_CLE_API' \
  'https://api.clore.ai/v1/marketplace'
```

**Réponse :**

```json
{
  "servers": [
    {
      "id": 6,
      "owner": 4,
      "mrl": 73,
      "price": {
        "on_demand": { "bitcoin": 0.00001 },
        "spot": { "bitcoin": 0.000001 }
      },
      "rented": false,
      "specs": {
        "cpu": "Intel Core i9-11900",
        "ram": 62.67,
        "gpu": "1x NVIDIA GeForce GTX 1080 Ti",
        "gpuram": 11,
        "net": { "up": 26.38, "down": 118.42, "cc": "CZ" }
      }
    }
  ],
  "my_servers": [1, 2, 4],
  "code": 0
}
```

***

### Récupérer vos commandes

```bash
curl -XGET \
  -H 'auth: VOTRE_CLE_API' \
  'https://api.clore.ai/v1/my_orders'
```

Inclure les commandes terminées/expirées :

```bash
curl -XGET \
  -H 'auth: VOTRE_CLE_API' \
  'https://api.clore.ai/v1/my_orders?return_completed=true'
```

***

### Créer une commande (à la demande)

```bash
curl -XPOST \
  -H 'auth: VOTRE_CLE_API' \
  -H 'Content-type: application/json' \
  -d '{
    "currency": "bitcoin",
    "image": "cloreai/ubuntu20.04-jupyter",
    "renting_server": 6,
    "type": "on-demand",
    "ports": {
      "22": "tcp",
      "8888": "http"
    },
    "ssh_password": "YourSSHPassword123",
    "jupyter_token": "YourJupyterToken123"
  }' \
  'https://api.clore.ai/v1/create_order'
```

**Réponse :**

```json
{ "code": 0 }
```

***

### Créer une commande Spot

Les commandes Spot sont moins chères mais peuvent être surenchéries. Vous fixez votre prix par jour :

```bash
curl -XPOST \
  -H 'auth: VOTRE_CLE_API' \
  -H 'Content-type: application/json' \
  -d '{
    "currency": "bitcoin",
    "image": "cloreai/ubuntu20.04-jupyter",
    "renting_server": 6,
    "type": "spot",
    "spotprice": 0.000005,
    "ports": {
      "22": "tcp",
      "8888": "http"
    },
    "ssh_password": "YourSSHPassword123"
  }' \
  'https://api.clore.ai/v1/create_order'
```

***

### Vérifier le statut d'une commande

```bash
curl -XGET \
  -H 'auth: VOTRE_CLE_API' \
  'https://api.clore.ai/v1/my_orders'
```

Les commandes actives incluent `pub_cluster` (noms d'hôtes) et `tcp_ports` pour l'accès SSH :

```json
{
  "id": 38,
  "pub_cluster": ["n1.c1.clorecloud.net", "n2.c1.clorecloud.net"],
  "tcp_ports": ["22:10000"],
  "http_port": "8888",
  "expired": false
}
```

Se connecter en SSH à votre serveur loué :

```bash
ssh root@n1.c1.clorecloud.net -p 10000
```

***

### Annuler une commande

```bash
curl -XPOST \
  -H 'auth: VOTRE_CLE_API' \
  -H 'Content-type: application/json' \
  -d '{
    "id": 38
  }' \
  'https://api.clore.ai/v1/cancel_order'
```

Signaler éventuellement un problème avec le serveur :

```bash
curl -XPOST \
  -H 'auth: VOTRE_CLE_API' \
  -H 'Content-type: application/json' \
  -d '{
    "id": 38,
    "issue": "Le GPU surchauffait et réduisait les performances"
  }' \
  'https://api.clore.ai/v1/cancel_order'
```

***

## SDK Python

Une couche légère utilisant la `bibliothèque requests` Installez-la avec :

```bash
pip install requests
```

### Classe CloreClient

```python
import requests
import time

class CloreClient:
    """SDK Python simple pour l'API REST de Clore.ai."""
    
    BASE_URL = "https://api.clore.ai/v1"
    
    def __init__(self, api_key: str):
        self.api_key = api_key
        self.session = requests.Session()
        self.session.headers.update({"auth": api_key})
    
    def _get(self, endpoint: str, params: dict = None) -> dict:
        url = f"{self.BASE_URL}/{endpoint}"
        response = self.session.get(url, params=params)
        response.raise_for_status()
        data = response.json()
        if data.get("code") != 0:
            raise CloreAPIError(data)
        return data
    
    def _post(self, endpoint: str, body: dict) -> dict:
        url = f"{self.BASE_URL}/{endpoint}"
        self.session.headers.update({"Content-type": "application/json"})
        response = self.session.post(url, json=body)
        response.raise_for_status()
        data = response.json()
        if data.get("code") != 0:
            raise CloreAPIError(data)
        return data
    
    def list_servers(self) -> list:
        """Obtenir tous les serveurs disponibles sur le marché."""
        data = self._get("marketplace")
        return data["servers"]
    
    def get_server_details(self, server_id: int) -> dict:
        """Obtenir les informations du marché spot pour un serveur spécifique."""
        data = self._get("spot_marketplace", params={"market": server_id})
        return data["market"]
    
    def create_order(
        self,
        server_id: int,
        image: str,
        order_type: str = "on-demand",
        currency: str = "bitcoin",
        spotprice: float = None,
        ports: dict = None,
        ssh_password: str = None,
        ssh_key: str = None,
        jupyter_token: str = None,
        env: dict = None,
        command: str = None,
    ) -> dict:
        """
        Créer une commande à la demande ou Spot.
        
        Args :
            server_id : ID du serveur à louer
            image : image Docker (ex. 'cloreai/ubuntu20.04-jupyter')
            order_type : 'on-demand' ou 'spot'
            currency : 'bitcoin' (par défaut)
            spotprice : Requis pour les commandes spot — prix par jour en BTC
            ports : Redirection de ports, ex. {"22": "tcp", "8888": "http"}
            ssh_password : Mot de passe SSH (caractères alphanumériques uniquement)
            ssh_key : Clé publique SSH
            jupyter_token : Jeton du notebook Jupyter
            env : Dictionnaire des variables d'environnement
            command : Commande shell à exécuter après le démarrage du conteneur
        """
        if order_type == "spot" and spotprice is None:
            raise ValueError("spotprice is required for spot orders")
        
        body = {
            "currency": currency,
            "image": image,
            "renting_server": server_id,
            "type": order_type,
        }
        
        if spotprice is not None:
            body["spotprice"] = spotprice
        if ports:
            body["ports"] = ports
        if ssh_password:
            body["ssh_password"] = ssh_password
        if ssh_key:
            body["ssh_key"] = ssh_key
        if jupyter_token:
            body["jupyter_token"] = jupyter_token
        if env:
            body["env"] = env
        if command:
            body["command"] = command
        
        return self._post("create_order", body)
    
    def get_orders(self, include_completed: bool = False) -> list:
        """Obtenir vos commandes actives (et éventuellement terminées)."""
        params = {"return_completed": "true"} if include_completed else {}
        data = self._get("my_orders", params=params)
        return data["orders"]
    
    def cancel_order(self, order_id: int, issue: str = None) -> dict:
        """Annuler une commande. Signaler éventuellement un problème."""
        body = {"id": order_id}
        if issue:
            body["issue"] = issue
        return self._post("cancel_order", body)
    
    def get_wallets(self) -> list:
        """Obtenir vos portefeuilles et soldes."""
        data = self._get("wallets")
        return data["wallets"]
    
    def get_my_servers(self) -> list:
        """Obtenir les serveurs que vous fournissez au marché."""
        data = self._get("my_servers")
        return data["servers"]


class CloreAPIError(Exception):
    """Levée lorsque l'API Clore retourne un code non nul."""
    
    ERROR_CODES = {
        0: "Normal",
        1: "Erreur de base de données",
        2: "Données d'entrée invalides",
        3: "Jeton API invalide",
        4: "Endpoint invalide",
        5: "Limite de débit dépassée (1 req/s)",
        6: "Erreur (voir le champ error)",
    }
    
    def __init__(self, response: dict):
        self.code = response.get("code")
        self.error = response.get("error", "")
        message = self.ERROR_CODES.get(self.code, f"Code inconnu {self.code}")
        if self.error:
            message = f"{message} : {self.error}"
        super().__init__(f"Erreur API Clore {self.code} : {message}")
```

### Exemple complet fonctionnel

```python
import os
from clore_client import CloreClient, CloreAPIError

# Initialiser le client
client = CloreClient(api_key=os.environ["CLORE_API_KEY"])

# 1. Parcourir le marché
servers = client.list_servers()
print(f"{len(servers)} serveurs trouvés sur le marché")

# 2. Filtrer les serveurs RTX 4090 disponibles
rtx4090_servers = [
    s for s in servers
    if "4090" in s["specs"].get("gpu", "") and not s["rented"]
]

if not rtx4090_servers:
    print("Aucun serveur RTX 4090 disponible trouvé")
    exit(1)

# 3. Choisir le moins cher
cheapest = min(rtx4090_servers, key=lambda s: s["price"]["on_demand"]["bitcoin"])
print(f"RTX 4090 la moins chère : ID du serveur {cheapest['id']}, "
      f"prix {cheapest['price']['on_demand']['bitcoin']:.8f} BTC/jour")

# 4. Créer une commande
try:
    client.create_order(
        server_id=cheapest["id"],
        image="cloreai/ubuntu20.04-jupyter",
        order_type="on-demand",
        currency="bitcoin",
        ports={"22": "tcp", "8888": "http"},
        ssh_password="SecurePass123",
        jupyter_token="MyToken123",
    )
    print("Commande créée avec succès !")
except CloreAPIError as e:
    print(f"Échec de la création de la commande : {e}")
    exit(1)

# 5. Vérifier vos commandes
orders = client.get_orders()
for order in orders:
    if not order.get("expired"):
        cluster = order.get("pub_cluster", [])
        tcp = order.get("tcp_ports", [])
        print(f"Commande {order['id']} : serveur {order['si']}")
        if cluster and tcp:
            ssh_port = tcp[0].split(":")[1]
            print(f"  SSH : ssh root@{cluster[0]} -p {ssh_port}")
```

***

## Exemple Node.js

Utilisation de la `fetch` API (Node.js 18+) :

```javascript
const BASE_URL = 'https://api.clore.ai/v1';

class CloreClient {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.defaultHeaders = {
      'auth': apiKey,
    };
  }

  async get(endpoint, params = {}) {
    const url = new URL(`${BASE_URL}/${endpoint}`);
    Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
    
    const res = await fetch(url.toString(), {
      headers: this.defaultHeaders,
    });
    
    const data = await res.json();
    if (data.code !== 0) {
      throw new Error(`Erreur API Clore ${data.code} : ${data.error || 'inconnu'}`);
    }
    return data;
  }

  async post(endpoint, body) {
    const res = await fetch(`${BASE_URL}/${endpoint}`, {
      method: 'POST',
      headers: {
        ...this.defaultHeaders,
        'Content-type': 'application/json',
      },
      body: JSON.stringify(body),
    });
    
    const data = await res.json();
    if (data.code !== 0) {
      throw new Error(`Erreur API Clore ${data.code} : ${data.error || 'inconnu'}`);
    }
    return data;
  }

  async listServers() {
    const data = await this.get('marketplace');
    return data.servers;
  }

  async getOrders(includeCompleted = false) {
    const params = includeCompleted ? { return_completed: 'true' } : {};
    const data = await this.get('my_orders', params);
    return data.orders;
  }

  async createOrder({ serverId, image, type = 'on-demand', currency = 'bitcoin', spotprice, ports, sshPassword, jupyterToken, env, command }) {
    const body = {
      currency,
      image,
      renting_server: serverId,
      type,
      ...(spotprice && { spotprice }),
      ...(ports && { ports }),
      ...(sshPassword && { ssh_password: sshPassword }),
      ...(jupyterToken && { jupyter_token: jupyterToken }),
      ...(env && { env }),
      ...(command && { command }),
    };
    return this.post('create_order', body);
  }

  async cancelOrder(orderId, issue = null) {
    const body = { id: orderId };
    if (issue) body.issue = issue;
    return this.post('cancel_order', body);
  }
}

// Exemple d'utilisation
const client = new CloreClient(process.env.CLORE_API_KEY);

async function main() {
  // Lister le marché
  const servers = await client.listServers();
  console.log(`Le marché contient ${servers.length} serveurs`);

  // Trouver les serveurs RTX 4090 disponibles
  const available = servers.filter(
    s => s.specs.gpu.includes('4090') && !s.rented
  );
  console.log(`Serveurs RTX 4090 disponibles : ${available.length}`);

  // Obtenir les commandes en cours
  const orders = await client.getOrders();
  const activeOrders = orders.filter(o => !o.expired);
  console.log(`Commandes actives : ${activeOrders.length}`);

  for (const order of activeOrders) {
    const host = order.pub_cluster?.[0];
    const portMapping = order.tcp_ports?.[0];
    if (host && portMapping) {
      const sshPort = portMapping.split(':')[1];
      console.log(`Commande ${order.id} → ssh root@${host} -p ${sshPort}`);
    }
  }
}

main().catch(console.error);
```

***

## Flux de travail courants

### Trouver le RTX 4090 le moins cher et le louer

```python
from clore_client import CloreClient

client = CloreClient(api_key="VOTRE_CLE_API")

def rent_cheapest_rtx4090(image="cloreai/ubuntu20.04-jupyter", ssh_password="SecurePass123"):
    servers = client.list_servers()
    
    # Filtre : RTX 4090 disponible
    candidates = [
        s for s in servers
        if "4090" in s["specs"].get("gpu", "")
        and not s["rented"]
    ]
    
    if not candidates:
        raise RuntimeError("Aucun serveur RTX 4090 disponible trouvé")
    
    # Trier par prix BTC à la demande
    candidates.sort(key=lambda s: s["price"]["on_demand"]["bitcoin"])
    best = candidates[0]
    
    price_btc = best["price"]["on_demand"]["bitcoin"]
    print(f"Location du serveur {best['id']}: {best['specs']['gpu']} @ {price_btc:.8f} BTC/jour")
    
    client.create_order(
        server_id=best["id"],
        image=image,
        order_type="on-demand",
        currency="bitcoin",
        ports={"22": "tcp"},
        ssh_password=ssh_password,
    )
    
    print("Terminé ! Vérifiez vos commandes pour les détails de connexion SSH.")
    return best["id"]

rent_cheapest_rtx4090()
```

***

### Surveiller mes commandes

```python
import time
from clore_client import CloreClient

client = CloreClient(api_key="VOTRE_CLE_API")

def monitor_orders(poll_interval_seconds=60):
    """Interroger les commandes et afficher les mises à jour de statut."""
    print(f"Surveillance des commandes (interrogation toutes les {poll_interval_seconds}s). Ctrl+C pour arrêter.\n")
    
    while True:
        orders = client.get_orders(include_completed=False)
        active = [o for o in orders if not o.get("expired")]
        
        print(f"--- {len(active)} commande(s) active(s) ---")
        for order in active:
            cluster = order.get("pub_cluster", [])
            tcp = order.get("tcp_ports", [])
            spend = order.get("spend", 0)
            
            ssh_info = ""
            if cluster and tcp:
                port = tcp[0].split(":")[1]
                ssh_info = f" | SSH : {cluster[0]}:{port}"
            
            print(f"  Commande {order['id']}: serveur {order['si']}"
                  f" | dépensé {spend:.8f} BTC{ssh_info}")
        
        if not active:
            print("  Aucune commande active.")
        
        print()
        time.sleep(poll_interval_seconds)

monitor_orders()
```

***

### Location automatique lorsque le prix descend en dessous de X

```python
import time
from clore_client import CloreClient

client = CloreClient(api_key="VOTRE_CLE_API")

def auto_rent_on_price_drop(
    gpu_model: str = "RTX 4090",
    max_price_btc: float = 0.00015,
    image: str = "cloreai/ubuntu20.04-jupyter",
    ssh_password: str = "SecurePass123",
    check_interval_seconds: int = 120,
):
    """
    Surveillez le marché et louez automatiquement un GPU lorsque le prix descend en dessous du seuil.
    
    Args :
        gpu_model : Nom du GPU à rechercher (insensible à la casse)
        max_price_btc : Prix maximal acceptable par jour en BTC
        image : Image Docker à déployer
        ssh_password : Mot de passe SSH pour le conteneur
        check_interval_seconds : Fréquence de vérification (respectez les limites de débit !)
    """
    print(f"Recherche de {gpu_model} à ≤ {max_price_btc:.8f} BTC/jour...")
    
    while True:
        servers = client.list_servers()
        
        for server in servers:
            gpu = server["specs"].get("gpu", "")
            if gpu_model.lower() not in gpu.lower():
                continue
            if server["rented"]:
                continue
            
            price = server["price"]["on_demand"]["bitcoin"]
            if price <= max_price_btc:
                print(f"🎯 Correspondance trouvée ! Serveur {server['id']}: {gpu} @ {price:.8f} BTC/jour")
                
                try:
                    client.create_order(
                        server_id=server["id"],
                        image=image,
                        order_type="on-demand",
                        currency="bitcoin",
                        ports={"22": "tcp"},
                        ssh_password=ssh_password,
                        required_price=price,  # Verrouiller ce prix
                    )
                    print(f"✅ Commande créée pour le serveur {server['id']} !")
                    return server["id"]
                except Exception as e:
                    print(f"Échec de la création de la commande : {e}. Nouvelle tentative en cours...")
        
        print(f"Pas encore de correspondance. Nouvelle vérification dans {check_interval_seconds}s...")
        time.sleep(check_interval_seconds)

auto_rent_on_price_drop(gpu_model="4090", max_price_btc=0.00012)
```

***

## WebSocket

L'API REST de Clore.ai n'expose pas actuellement de point de terminaison WebSocket. Pour la surveillance en temps réel, utilisez le sondage avec un intervalle raisonnable (voir les limites de fréquence ci-dessous).

***

## Limites de fréquence

| Point de terminaison                 | Limite                               |
| ------------------------------------ | ------------------------------------ |
| La plupart des points de terminaison | **1 requête/seconde**                |
| `create_order`                       | **1 requête/5 secondes**             |
| `set_spot_price` (réduction de prix) | Une fois toutes les **600 secondes** |

**Réponse de limitation de fréquence (code 5) :**

```json
{ "code": 5 }
```

**Bonnes pratiques :**

* Ajouter `time.sleep(1)` entre des appels API consécutifs
* Pour `create_order`, attendez au moins 5 secondes entre les requêtes
* Utilisez des intervalles de sondage de 60+ secondes pour les boucles de surveillance
* Mettez en cache les données du marché localement si vous devez les interroger fréquemment

**Assistant Python :**

```python
import time

def safe_api_call(fn, *args, delay=1.1, **kwargs):
    """Enveloppe un appel API avec une sécurité contre les limites de fréquence."""
    result = fn(*args, **kwargs)
    time.sleep(delay)
    return result
```

***

## Gestion des erreurs

Chaque réponse API inclut un `code` champ. Une valeur de `0` signifie succès.

### Codes d'erreur

| Code | Signification                              | Action                                            |
| ---- | ------------------------------------------ | ------------------------------------------------- |
| `0`  | Succès                                     | —                                                 |
| `1`  | Erreur de base de données                  | Réessayer après un délai                          |
| `2`  | Données d'entrée invalides                 | Vérifiez le corps/les paramètres de votre requête |
| `3`  | Jeton API invalide                         | Vérifiez votre clé API dans le tableau de bord    |
| `4`  | Point de terminaison invalide              | Vérifiez l'URL du point de terminaison            |
| `5`  | Limite de fréquence dépassée (1 req/sec)   | Ajoutez des délais entre les requêtes             |
| `6`  | Erreur d'application (voir `erreur` champ) | Lisez le `erreur` champ pour des détails          |

### Sous-erreurs du code 6

| `erreur` valeur               | Signification                                                                             |
| ----------------------------- | ----------------------------------------------------------------------------------------- |
| `exceeded_max_step`           | Réduction du prix spot trop importante ; vérifiez `max_step` champ                        |
| `can_lower_every_600_seconds` | Vous devez attendre avant de baisser à nouveau le prix spot ; vérifiez `time_to_lowering` |

### Exemple de gestion d'erreur en Python

```python
from clore_client import CloreClient, CloreAPIError
import time

client = CloreClient(api_key="VOTRE_CLE_API")

def create_order_with_retry(server_id, image, max_retries=3):
    for attempt in range(max_retries):
        try:
            return client.create_order(
                server_id=server_id,
                image=image,
                order_type="on-demand",
                currency="bitcoin",
                ports={"22": "tcp"},
                ssh_password="SecurePass123",
            )
        except CloreAPIError as e:
            if e.code == 5:  # Rate limit
                print(f"Limité par le débit. Attente de 5s... (tentative {attempt+1}/{max_retries})")
                time.sleep(5)
            elif e.code == 3:  # Bad API key
                print("Clé API invalide ! Vérifiez votre CLORE_API_KEY.")
                raise
            elif e.code == 2:  # Bad input
                print(f"Requête invalide : {e}")
                raise
            else:
                print(f"Erreur API : {e}. Nouvelle tentative dans 3s...")
                time.sleep(3)
    
    raise RuntimeError(f"Échec après {max_retries} tentatives")
```

### Exemple de gestion d'erreur en JavaScript

```javascript
async function createOrderWithRetry(client, serverConfig, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await client.createOrder(serverConfig);
    } catch (err) {
      const code = parseInt(err.message.match(/error (\d+)/)?.[1]);
      
      if (code === 5) {
        console.log(`Limité par le débit. Attente de 5s... (tentative ${attempt + 1}/${maxRetries})`);
        await new Promise(r => setTimeout(r, 5000));
      } else if (code === 3) {
        throw new Error('Clé API invalide');
      } else {
        console.log(`Erreur API : ${err.message}. Nouvelle tentative dans 3s...`);
        await new Promise(r => setTimeout(r, 3000));
      }
    }
  }
  throw new Error(`Échec après ${maxRetries} tentatives`);
}
```

***

## Images Docker disponibles

Clore.ai fournit des images préconstruites optimisées pour les charges GPU :

| Image                         | Description               |
| ----------------------------- | ------------------------- |
| `cloreai/ubuntu20.04-jupyter` | Ubuntu 20.04 + JupyterLab |
| `cloreai/ubuntu22.04-jupyter` | Ubuntu 22.04 + JupyterLab |

Vous pouvez également utiliser n'importe quelle image publique de Docker Hub. Pour l'accès GPU, utilisez des images compatibles CUDA, par ex :

```
nvidia/cuda:12.1.0-devel-ubuntu22.04
```

***

## Transfert de ports

Lors de la création d'une commande, spécifiez les ports à exposer :

```json
{
  "ports": {
    "22": "tcp",
    "8888": "http",
    "6006": "http"
  }
}
```

* **`"tcp"`** — Transfert de port TCP direct (pour SSH, serveurs personnalisés)
* **`"http"`** — Proxy HTTPS (pour Jupyter, interfaces web). Un seul port HTTP autorisé par `http_port` champ.

Après la création de la commande, les détails de connexion apparaissent dans `my_orders`:

```json
{
  "pub_cluster": ["n1.c1.clorecloud.net", "n2.c1.clorecloud.net"],
  "tcp_ports": ["22:10000"],
  "http_port": "8888"
}
```

Se connecter via SSH :

```bash
ssh root@n1.c1.clorecloud.net -p 10000
```

Accéder à Jupyter via le navigateur : `https://n1.c1.clorecloud.net` (utilise le `http_port`)
