]> git.digitality.be Git - pdw25-26/commitdiff
fix: gateway WS proxy, price PORT env, strategy stub, scripts ps1
authorSteph Ponzo <ponzo.stephane2@gmail.com>
Fri, 27 Feb 2026 19:51:44 +0000 (20:51 +0100)
committerSteph Ponzo <ponzo.stephane2@gmail.com>
Fri, 27 Feb 2026 19:51:44 +0000 (20:51 +0100)
Wallette/gateway/gateway.js
Wallette/server/modules/price/server.js
Wallette/server/modules/strategy/server.js [new file with mode: 0644]
Wallette/server/package.json
Wallette/start-all.ps1
Wallette/stop-all.bat [new file with mode: 0644]
Wallette/web/socketService.js

index 239035deb17c7cd24f0acc8078a44977481f176d..9cfa0a80f7466007396cd620921561ec60e52a42 100644 (file)
@@ -8,20 +8,24 @@
 //
 // ROUTING TABLE :
 //   /api/price/*    → PRICE_BASE_URL    (défaut: http://127.0.0.1:3001)
+//   /api/pairs      → PRICE_BASE_URL    (exposition directe des paires)
 //   /api/alerts/*   → ALERTS_BASE_URL   (défaut: http://127.0.0.1:3003)
+//   /api/wallets/*  → WALLET_BASE_URL   (défaut: http://127.0.0.1:3004)
 //   /api/strategy/* → STRATEGY_BASE_URL (défaut: http://127.0.0.1:3002)
+//   /socket.io/*    → ALERTS_BASE_URL   (WebSocket proxy)
 //
 // PATH REWRITE :
-//   Les services exposent leurs routes avec le préfixe complet /api/*
-//   Le gateway transmet donc le path COMPLET tel quel.
+//   Le path est transmis COMPLET tel quel aux services.
 //   Ex: GET /api/price/current → http://127.0.0.1:3001/api/price/current
 // =========================================================
 
 import dotenv from 'dotenv';
-import path from 'path';
-import { fileURLToPath } from 'url';
 import http from 'http';
 import https from 'https';
+import net from 'net';
+import tls from 'tls';
+import path from 'path';
+import { fileURLToPath } from 'url';
 
 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);
@@ -31,191 +35,284 @@ dotenv.config({ path: path.resolve(__dirname, '../server/.env') });
 // CONFIGURATION
 // =========================================================
 
-const GATEWAY_PORT = process.env.GATEWAY_PORT || process.env.PORT || 3000;
+const GATEWAY_PORT = Number(process.env.PORT || process.env.GATEWAY_PORT) || 3000;
 
-const UPSTREAMS = {
-  '/api/price':    process.env.PRICE_BASE_URL    || 'http://127.0.0.1:3001',
-  '/api/alerts':   process.env.ALERTS_BASE_URL   || 'http://127.0.0.1:3003',
-  '/api/strategy': process.env.STRATEGY_BASE_URL || 'http://127.0.0.1:3002',
+const SERVICE_BASE_URLS = {
+  price:    process.env.PRICE_BASE_URL    || 'http://127.0.0.1:3001',
+  alerts:   process.env.ALERTS_BASE_URL   || 'http://127.0.0.1:3003',
+  strategy: process.env.STRATEGY_BASE_URL || 'http://127.0.0.1:3002',
+  wallet:   process.env.WALLET_BASE_URL   || 'http://127.0.0.1:3004',
 };
 
+// Tri par longueur décroissante pour que les préfixes longs matchent en premier
+const ROUTES = [
+  // WebSocket Socket.IO — proxy TCP direct vers alerts
+  { prefix: '/socket.io', upstream: SERVICE_BASE_URLS.alerts,   name: 'alerts-service',   ws: true },
+
+  // API REST
+  { prefix: '/api/alerts',   upstream: SERVICE_BASE_URLS.alerts,   name: 'alerts-service'   },
+  { prefix: '/api/wallets',  upstream: SERVICE_BASE_URLS.wallet,   name: 'wallet-service'   },
+  { prefix: '/api/price',    upstream: SERVICE_BASE_URLS.price,    name: 'price-service'    },
+  { prefix: '/api/pairs',    upstream: SERVICE_BASE_URLS.price,    name: 'price-service'    },
+  { prefix: '/api/strategy', upstream: SERVICE_BASE_URLS.strategy, name: 'strategy-service' },
+].sort((a, b) => b.prefix.length - a.prefix.length);
+
 const PROXY_TIMEOUT_MS = 5000;
 
 // =========================================================
-// SERVEUR HTTP NATIF (Node >= 18, pas de dépendances lourdes)
+// HELPERS CORS
 // =========================================================
 
-const server = http.createServer(async (req, res) => {
-  const url = req.url || '/';
-
-  // -------------------------------------------------------
-  // ROUTES INTERNES DU GATEWAY
-  // -------------------------------------------------------
+function applyCorsHeaders(headers) {
+  headers['access-control-allow-origin']  = '*';
+  headers['access-control-allow-methods'] = 'GET,POST,PUT,PATCH,DELETE,OPTIONS';
+  headers['access-control-allow-headers'] = 'Content-Type, Authorization';
+  headers['access-control-max-age']       = '86400';
+  return headers;
+}
 
-  // GET /api/gateway/routes → table de routing (dev)
-  if (req.method === 'GET' && url === '/api/gateway/routes') {
-    return sendJSON(res, 200, {
-      ok: true,
-      data: {
-        gateway_port: GATEWAY_PORT,
-        path_rewrite: 'FULL PATH (path transmis tel quel aux services)',
-        routes: Object.entries(UPSTREAMS).map(([prefix, upstream]) => ({
-          prefix,
-          upstream,
-          example: `${prefix}/... → ${upstream}${prefix}/...`
-        }))
-      }
-    });
-  }
+function sendJSON(res, statusCode, payload) {
+  const body = JSON.stringify(payload);
+  const headers = applyCorsHeaders({
+    'content-type':   'application/json; charset=utf-8',
+    'content-length': Buffer.byteLength(body),
+  });
+  res.writeHead(statusCode, headers);
+  res.end(body);
+}
 
-  // GET /api/gateway/health → ping des services upstream
-  if (req.method === 'GET' && url === '/api/gateway/health') {
-    const checks = await Promise.all(
-      Object.entries(UPSTREAMS).map(async ([prefix, upstream]) => {
-        const healthUrl = `${upstream}/health`;
-        try {
-          const result = await proxyFetch('GET', healthUrl, null, 2000);
-          return { prefix, upstream, status: result.status, ok: result.status < 400 };
-        } catch (e) {
-          return { prefix, upstream, status: 'DOWN', ok: false, error: e.message };
-        }
-      })
-    );
-    const allOk = checks.every(c => c.ok);
-    return sendJSON(res, allOk ? 200 : 207, { ok: allOk, data: { services: checks } });
-  }
+// =========================================================
+// ROUTE MATCHING
+// =========================================================
 
-  // -------------------------------------------------------
-  // PROXY VERS LES SERVICES
-  // -------------------------------------------------------
+function matchRoute(url = '') {
+  return ROUTES.find(r => url.startsWith(r.prefix)) || null;
+}
 
-  // Trouver quel upstream correspond au path
-  const matchedPrefix = Object.keys(UPSTREAMS).find(prefix => url.startsWith(prefix));
+function pickHttpLib(protocol) {
+  return protocol === 'https:' ? https : http;
+}
 
-  if (!matchedPrefix) {
-    return sendJSON(res, 404, { ok: false, error: { code: 'NOT_FOUND', message: `No route for ${url}` } });
-  }
+// Retire les hop-by-hop headers (RFC 7230) avant de transmettre
+function cleanHopByHop(headers) {
+  const hopByHop = [
+    'connection', 'keep-alive', 'proxy-authenticate',
+    'proxy-authorization', 'te', 'trailers',
+    'transfer-encoding', 'upgrade', 'proxy-connection',
+  ];
+  for (const h of hopByHop) delete headers[h];
+}
 
-  const upstreamBase = UPSTREAMS[matchedPrefix];
+// =========================================================
+// HEALTH CHECKS
+// =========================================================
 
-  // PATH REWRITE : on garde le path complet (les services exposent /api/...)
-  const targetUrl = upstreamBase + url;
+function checkHealth(serviceName, baseUrl, timeoutMs = 1000) {
+  return new Promise(resolve => {
+    const upstream = new URL(baseUrl);
+    const lib = pickHttpLib(upstream.protocol);
+
+    const r = lib.request({
+      hostname: upstream.hostname,
+      port:     upstream.port || (upstream.protocol === 'https:' ? 443 : 80),
+      method:   'GET',
+      path:     '/health',
+      headers:  { host: upstream.host },
+      timeout:  timeoutMs,
+    }, upRes => {
+      upRes.resume();
+      resolve({
+        service: serviceName,
+        url:     baseUrl,
+        ok:      (upRes.statusCode || 500) < 400,
+        status:  upRes.statusCode,
+      });
+    });
 
-  // Lire le body pour les méthodes non-GET/HEAD
-  let body = null;
-  if (!['GET', 'HEAD'].includes(req.method)) {
-    body = await readBody(req);
-  }
+    r.on('timeout', () => {
+      r.destroy();
+      resolve({ service: serviceName, url: baseUrl, ok: false, status: 'TIMEOUT' });
+    });
+    r.on('error', err => {
+      resolve({ service: serviceName, url: baseUrl, ok: false, status: 'DOWN', error: err.message });
+    });
+    r.end();
+  });
+}
 
-  console.log(`[gateway] ${req.method} ${url} → ${targetUrl}`);
+// =========================================================
+// HTTP PROXY
+// =========================================================
 
-  try {
-    const result = await proxyFetch(req.method, targetUrl, body, PROXY_TIMEOUT_MS, req.headers);
+function proxyHttpRequest(req, res, route) {
+  const incomingUrl = req.url || '/';
+  const upstream    = new URL(route.upstream);
+  const lib         = pickHttpLib(upstream.protocol);
+  const startedAt   = Date.now();
+
+  const headers = { ...req.headers };
+  headers.host                  = upstream.host;
+  headers['x-forwarded-host']   = req.headers.host || '';
+  headers['x-forwarded-proto']  = 'http';
+  headers['x-forwarded-for']    = req.socket?.remoteAddress || '';
+  cleanHopByHop(headers);
+
+  const upstreamReq = lib.request({
+    hostname: upstream.hostname,
+    port:     upstream.port || (upstream.protocol === 'https:' ? 443 : 80),
+    method:   req.method,
+    path:     incomingUrl,
+    headers,
+    timeout:  PROXY_TIMEOUT_MS,
+  }, upRes => {
+    const outHeaders = applyCorsHeaders({ ...upRes.headers });
+    outHeaders['x-gateway-upstream'] = route.name;
+    res.writeHead(upRes.statusCode || 500, outHeaders);
+    upRes.pipe(res);
+
+    res.on('finish', () => {
+      const ms = Date.now() - startedAt;
+      console.log(`[gateway] ${req.method} ${incomingUrl} → ${route.upstream}${incomingUrl} (${upRes.statusCode}) ${ms}ms`);
+    });
+  });
 
-    // Retransmettre le statut et le body au client
-    res.writeHead(result.status, { 'Content-Type': 'application/json' });
-    res.end(result.body);
+  upstreamReq.on('timeout', () => upstreamReq.destroy(new Error(`Timeout after ${PROXY_TIMEOUT_MS}ms`)));
 
-    console.log(`[gateway] ${req.method} ${url} → ${targetUrl} (${result.status})`);
-  } catch (err) {
-    // Service upstream injoignable → 502
-    const serviceName = matchedPrefix.replace('/api/', '') + '-service';
-    console.error(`[gateway] UPSTREAM_DOWN: ${targetUrl} — ${err.message}`);
-    return sendJSON(res, 502, {
+  upstreamReq.on('error', err => {
+    if (res.headersSent) { try { res.end(); } catch {} return; }
+    console.error(`[gateway] UPSTREAM_DOWN ${route.name}: ${err.message}`);
+    sendJSON(res, 502, {
       ok: false,
-      error: {
-        code: 'UPSTREAM_DOWN',
-        message: `${serviceName} unavailable`
-      }
+      error: { code: 'UPSTREAM_DOWN', message: `${route.name} unavailable` },
     });
-  }
-});
+  });
+
+  req.on('aborted', () => upstreamReq.destroy());
+  res.on('close',   () => upstreamReq.destroy());
+  req.pipe(upstreamReq);
+}
 
 // =========================================================
-// HELPERS
+// WEBSOCKET PROXY (Socket.IO via /socket.io/*)
 // =========================================================
 
-/**
- * Lit le body d'une requête entrante et retourne un Buffer
- */
-function readBody(req) {
-  return new Promise((resolve, reject) => {
-    const chunks = [];
-    req.on('data', chunk => chunks.push(chunk));
-    req.on('end', () => resolve(Buffer.concat(chunks)));
-    req.on('error', reject);
-  });
-}
+function proxyWebSocketUpgrade(req, clientSocket, head) {
+  const incomingUrl = req.url || '/';
 
-/**
- * Envoie une requête HTTP vers un upstream
- * @param {string} method
- * @param {string} url
- * @param {Buffer|null} body
- * @param {number} timeoutMs
- * @param {object} [incomingHeaders]
- * @returns {Promise<{status: number, body: string}>}
- */
-function proxyFetch(method, url, body, timeoutMs = PROXY_TIMEOUT_MS, incomingHeaders = {}) {
-  return new Promise((resolve, reject) => {
-    const parsed = new URL(url);
-    const isHttps = parsed.protocol === 'https:';
-    const lib = isHttps ? https : http;
-
-    // Headers à transmettre (on filtre les headers problématiques)
-    const headers = {
-      'Content-Type': 'application/json',
-    };
-    if (incomingHeaders['authorization']) headers['Authorization'] = incomingHeaders['authorization'];
-    if (body && body.length > 0) headers['Content-Length'] = body.length;
-
-    const options = {
-      hostname: parsed.hostname,
-      port: parsed.port || (isHttps ? 443 : 80),
-      path: parsed.pathname + parsed.search,
-      method,
-      headers,
-      timeout: timeoutMs,
-    };
-
-    const proxyReq = lib.request(options, (proxyRes) => {
-      const chunks = [];
-      proxyRes.on('data', chunk => chunks.push(chunk));
-      proxyRes.on('end', () => {
-        resolve({
-          status: proxyRes.statusCode,
-          body: Buffer.concat(chunks).toString('utf8')
-        });
-      });
-    });
+  if (!incomingUrl.startsWith('/socket.io')) {
+    clientSocket.destroy();
+    return;
+  }
 
-    proxyReq.on('timeout', () => {
-      proxyReq.destroy();
-      reject(new Error(`Timeout after ${timeoutMs}ms`));
-    });
+  const upstream = new URL(SERVICE_BASE_URLS.alerts);
+  const port     = Number(upstream.port) || (upstream.protocol === 'https:' ? 443 : 80);
+  const connect  = upstream.protocol === 'https:' ? tls.connect : net.connect;
+
+  const upstreamSocket = connect({ host: upstream.hostname, port });
 
-    proxyReq.on('error', reject);
+  upstreamSocket.on('connect', () => {
+    const headers = { ...req.headers, host: upstream.host };
+    delete headers['proxy-connection'];
 
-    if (body && body.length > 0) {
-      proxyReq.write(body);
+    let headerLines = '';
+    for (const [key, value] of Object.entries(headers)) {
+      if (value === undefined) continue;
+      const v = Array.isArray(value) ? value.join(',') : String(value);
+      headerLines += `${key}: ${v}\r\n`;
     }
 
-    proxyReq.end();
+    upstreamSocket.write(`${req.method || 'GET'} ${incomingUrl} HTTP/1.1\r\n` + headerLines + '\r\n');
+    if (head && head.length > 0) upstreamSocket.write(head);
+
+    upstreamSocket.pipe(clientSocket);
+    clientSocket.pipe(upstreamSocket);
+    clientSocket.resume();
+
+    console.log(`[gateway][ws] UPGRADE ${incomingUrl} → ${upstream.origin}${incomingUrl}`);
   });
-}
 
-/**
- * Envoie une réponse JSON
- */
-function sendJSON(res, status, data) {
-  const body = JSON.stringify(data);
-  res.writeHead(status, {
-    'Content-Type': 'application/json',
-    'Content-Length': Buffer.byteLength(body)
+  upstreamSocket.on('error', err => {
+    console.error(`[gateway][ws] UPSTREAM_DOWN alerts-service: ${err.message}`);
+    try { clientSocket.end(); } catch {}
   });
-  res.end(body);
+
+  clientSocket.on('error', () => { try { upstreamSocket.end(); } catch {} });
 }
 
+// =========================================================
+// SERVEUR HTTP
+// =========================================================
+
+const server = http.createServer(async (req, res) => {
+  const url = req.url || '/';
+
+  // Preflight CORS
+  if (req.method === 'OPTIONS') {
+    res.writeHead(204, applyCorsHeaders({}));
+    res.end();
+    return;
+  }
+
+  // Health gateway
+  if (req.method === 'GET' && url === '/health') {
+    return sendJSON(res, 200, { ok: true, data: { service: 'gateway', port: GATEWAY_PORT } });
+  }
+
+  // Table de routing (dev)
+  if (req.method === 'GET' && url === '/api/gateway/routes') {
+    return sendJSON(res, 200, {
+      ok: true,
+      data: {
+        gateway_port: GATEWAY_PORT,
+        path_rewrite: 'FULL PATH (path transmis tel quel aux services)',
+        routes: ROUTES.map(r => ({
+          prefix:    r.prefix,
+          upstream:  r.upstream,
+          service:   r.name,
+          websocket: Boolean(r.ws),
+        })),
+      },
+    });
+  }
+
+  // Health de tous les services
+  if (req.method === 'GET' && url === '/api/gateway/health') {
+    const checks = await Promise.all([
+      checkHealth('price-service',    SERVICE_BASE_URLS.price),
+      checkHealth('alerts-service',   SERVICE_BASE_URLS.alerts),
+      checkHealth('wallet-service',   SERVICE_BASE_URLS.wallet),
+      checkHealth('strategy-service', SERVICE_BASE_URLS.strategy),
+    ]);
+
+    const required   = ['price-service', 'alerts-service', 'wallet-service'];
+    const requiredOk = checks.filter(c => required.includes(c.service)).every(c => c.ok);
+
+    return sendJSON(res, requiredOk ? 200 : 207, {
+      ok: requiredOk,
+      data: {
+        gateway:  { ok: true, port: GATEWAY_PORT },
+        services: checks.reduce((acc, c) => { acc[c.service] = c; return acc; }, {}),
+      },
+    });
+  }
+
+  // Proxy vers les services
+  const route = matchRoute(url);
+  if (!route) {
+    return sendJSON(res, 404, {
+      ok: false,
+      error: { code: 'NOT_FOUND', message: `No route for ${url}` },
+    });
+  }
+
+  return proxyHttpRequest(req, res, route);
+});
+
+// Proxy WebSocket (Socket.IO upgrade)
+server.on('upgrade', (req, socket, head) => {
+  proxyWebSocketUpgrade(req, socket, head);
+});
+
 // =========================================================
 // DÉMARRAGE
 // =========================================================
@@ -225,14 +322,18 @@ server.listen(GATEWAY_PORT, () => {
 ╔══════════════════════════════════════════════════════════╗
 ║                  API GATEWAY - Wall-e-tte                ║
 ╠══════════════════════════════════════════════════════════╣
-║  Écoute sur  : http://localhost:${GATEWAY_PORT}                  ║
+║  Écoute sur  : http://localhost:${GATEWAY_PORT}                     
 ║                                                          ║
 ║  ROUTING :                                               ║
-║   /api/price/*    → ${UPSTREAMS['/api/price']}        ║
-║   /api/alerts/*   → ${UPSTREAMS['/api/alerts']}       ║
-║   /api/strategy/* → ${UPSTREAMS['/api/strategy']}     ║
+║   /api/price/*    → ${SERVICE_BASE_URLS.price}              ║
+║   /api/pairs      → ${SERVICE_BASE_URLS.price}              ║
+║   /api/alerts/*   → ${SERVICE_BASE_URLS.alerts}             ║
+║   /socket.io/*    → ${SERVICE_BASE_URLS.alerts} (WS+HTTP)   ║
+║   /api/wallets/*  → ${SERVICE_BASE_URLS.wallet}             ║
+║   /api/strategy/* → ${SERVICE_BASE_URLS.strategy}           ║
 ║                                                          ║
 ║  ENDPOINTS GATEWAY :                                     ║
+║   GET /health              → gateway ok                  ║
 ║   GET /api/gateway/routes  → table de routing            ║
 ║   GET /api/gateway/health  → état des services           ║
 ╚══════════════════════════════════════════════════════════╝`);
index f652eec23a1a4cea29d1152ebdb3428b2b26cef9..1a1b88276c5b06aff3a5a73e77bc3514859832ef 100644 (file)
@@ -6,7 +6,7 @@ import { getActivePairIdByCode, getPairById, listActivePairs } from "./repositor
 import { insertPricePoint, getCurrentPrice, getPriceHistory } from "./repositories/price.repository.js";
 
 const app = express();
-const PORT = 3000;
+const PORT = process.env.PRICE_PORT || 3001;
 
 // On fetch souvent pour construire OHLC (bougie 5 min)
 const SAMPLE_INTERVAL_MS = 10 * 1000; // 10s
@@ -283,4 +283,4 @@ app.listen(PORT, async () => {
   } catch (e) {
     console.error("Erreur démarrage serveur:", e.message);
   }
-});
\ No newline at end of file
+});
diff --git a/Wallette/server/modules/strategy/server.js b/Wallette/server/modules/strategy/server.js
new file mode 100644 (file)
index 0000000..7e6d8c7
--- /dev/null
@@ -0,0 +1,80 @@
+// =========================================================
+// STRATEGY SERVICE - Serveur standalone (stub)
+// =========================================================
+// Ce fichier est volontairement minimal.
+// Son rôle : avoir un process indépendant sur le port 3002
+// pour que le gateway ne retourne pas 502 sur /api/strategy/*
+// et pour que le health check soit vert au démarrage.
+//
+// USAGE : node modules/strategy/server.js (depuis Wallette/server/)
+// PORT  : process.env.STRATEGY_PORT || 3002
+//
+// QUAND Sacha aura finalisé son module, ce fichier sera
+// remplacé par un vrai serveur avec les routes strategy.
+// =========================================================
+
+import cors from 'cors';
+import dotenv from 'dotenv';
+import express from 'express';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname  = path.dirname(__filename);
+dotenv.config({ path: path.resolve(__dirname, '../../.env') });
+
+const PORT = process.env.STRATEGY_PORT || 3002;
+
+const app = express();
+app.use(cors());
+app.use(express.json());
+
+// =========================================================
+// HEALTH CHECK (requis par le gateway)
+// =========================================================
+app.get('/health', (req, res) => {
+  res.json({
+    ok:      true,
+    service: 'strategy-service',
+    port:    PORT,
+    status:  'stub — module Sacha en cours d\'intégration',
+  });
+});
+
+// =========================================================
+// STUB ROUTES /api/strategy/*
+// =========================================================
+// Répond 501 (Not Implemented) pour toutes les routes
+// au lieu de laisser le gateway retourner 502.
+app.use('/api/strategy', (req, res) => {
+  res.status(501).json({
+    ok:    false,
+    error: {
+      code:    'NOT_IMPLEMENTED',
+      message: 'strategy-service non encore disponible',
+    },
+  });
+});
+
+// =========================================================
+// 404
+// =========================================================
+app.use((req, res) => {
+  res.status(404).json({
+    ok:    false,
+    error: { code: 'NOT_FOUND', message: 'Route non trouvée' },
+  });
+});
+
+// =========================================================
+// DÉMARRAGE
+// =========================================================
+app.listen(PORT, () => {
+  console.log(`
+╔══════════════════════════════════════════╗
+║      STRATEGY SERVICE (STUB)             ║
+║  HTTP : http://localhost:${PORT}             ║
+║  → /health          = OK                 ║
+║  → /api/strategy/*  = 501                ║
+╚══════════════════════════════════════════╝`);
+});
index 1f06fd738af28c8779bf3e33592bc777da8d2363..0da6dd7fb26032e2fef3a6549b95af8bd95ddeae 100644 (file)
     "test": "node test-module-complet.js",
     "test:server": "node test-server-socket.js",
     "alerts": "node modules/alerts/server.js",
-    "alerts:dev": "node --watch modules/alerts/server.js"
+    "alerts:dev": "node --watch modules/alerts/server.js",
+    "wallet": "node modules/wallet/server.js",
+    "wallet:dev": "node --watch modules/wallet/server.js",
+    "price": "node modules/price/server.js",
+    "price:dev": "node --watch modules/price/server.js",
+    "strategy": "node modules/strategy/server.js",
+    "strategy:dev": "node --watch modules/strategy/server.js"
   },
   "keywords": [
     "crypto",
index 6a978e2a48ec13961138ff7ed8573f7b5cadc551..151f08bc8aaadeeb5b4c5c057c1b7f752eddb9c5 100644 (file)
@@ -7,30 +7,48 @@ $ROOT = Split-Path -Parent $MyInvocation.MyCommand.Path
 
 Write-Host "[Wall-e-tte] Demarrage des services..." -ForegroundColor Cyan
 
+# ─── Vérifier .env ─────────────────────────────────────
+if (-not (Test-Path "$ROOT\server\.env")) {
+    Write-Host ""
+    Write-Host "[WARN] Aucun fichier server\.env detecte." -ForegroundColor Red
+    Write-Host "       Copie .env.example vers server\.env puis complete DB_*." -ForegroundColor Red
+    Write-Host ""
+}
+
 # ─── PRICE SERVICE (port 3001) ───────────────────────────
-Write-Host "[1/4] Lancement price-service sur :3001" -ForegroundColor Yellow
+Write-Host "[1/5] Lancement price-service sur :3001" -ForegroundColor Yellow
 Start-Process "cmd.exe" -ArgumentList "/k", "cd /d `"$ROOT\server`" && node modules/price/server.js" -WindowStyle Normal
 
 Start-Sleep -Seconds 2
 
 # ─── ALERTS SERVICE (port 3003) ──────────────────────────
-Write-Host "[2/4] Lancement alerts-service sur :3003" -ForegroundColor Yellow
+Write-Host "[2/5] Lancement alerts-service sur :3003" -ForegroundColor Yellow
 Start-Process "cmd.exe" -ArgumentList "/k", "cd /d `"$ROOT\server`" && node modules/alerts/server.js" -WindowStyle Normal
 
 Start-Sleep -Seconds 2
 
-# ─── STRATEGY SERVICE (port 3002) - optionnel ─────────────
+# ─── STRATEGY SERVICE (port 3002) ─────────────────────────
 $strategyPath = Join-Path $ROOT "server\modules\strategy\server.js"
 if (Test-Path $strategyPath) {
-    Write-Host "[3/4] Lancement strategy-service sur :3002" -ForegroundColor Yellow
+    Write-Host "[3/5] Lancement strategy-service sur :3002" -ForegroundColor Yellow
     Start-Process "cmd.exe" -ArgumentList "/k", "cd /d `"$ROOT\server`" && node modules/strategy/server.js" -WindowStyle Normal
     Start-Sleep -Seconds 2
 } else {
-    Write-Host "[3/4] strategy-service non trouve - le gateway retournera 502 pour /api/strategy/*" -ForegroundColor DarkYellow
+    Write-Host "[3/5] strategy-service non trouve - le gateway retournera 502 pour /api/strategy/*" -ForegroundColor DarkYellow
+}
+
+# ─── WALLET SERVICE (port 3004) ──────────────────────────
+$walletPath = Join-Path $ROOT "server\modules\wallet\server.js"
+if (Test-Path $walletPath) {
+    Write-Host "[4/5] Lancement wallet-service sur :3004" -ForegroundColor Yellow
+    Start-Process "cmd.exe" -ArgumentList "/k", "cd /d `"$ROOT\server`" && node modules/wallet/server.js" -WindowStyle Normal
+    Start-Sleep -Seconds 2
+} else {
+    Write-Host "[4/5] wallet-service non trouve - le gateway retournera 502 pour /api/wallets/*" -ForegroundColor DarkYellow
 }
 
 # ─── GATEWAY (port 3000) ─────────────────────────────────
-Write-Host "[4/4] Lancement gateway sur :3000" -ForegroundColor Yellow
+Write-Host "[5/5] Lancement gateway sur :3000" -ForegroundColor Yellow
 Start-Process "cmd.exe" -ArgumentList "/k", "cd /d `"$ROOT\gateway`" && node gateway.js" -WindowStyle Normal
 
 Write-Host ""
@@ -38,11 +56,16 @@ Write-Host "Tous les services sont lances !" -ForegroundColor Green
 Write-Host ""
 Write-Host "  Gateway  : http://localhost:3000" -ForegroundColor Cyan
 Write-Host "  Price    : http://localhost:3001" -ForegroundColor Cyan
+Write-Host "  Strategy : http://localhost:3002" -ForegroundColor Cyan
 Write-Host "  Alerts   : http://localhost:3003" -ForegroundColor Cyan
+Write-Host "  Wallet   : http://localhost:3004" -ForegroundColor Cyan
 Write-Host ""
 Write-Host "Tests rapides :"
+Write-Host "  curl http://localhost:3000/health"
 Write-Host "  curl http://localhost:3000/api/gateway/health"
 Write-Host "  curl 'http://localhost:3000/api/price/current?pair=BTC/EUR'"
 Write-Host "  curl 'http://localhost:3000/api/alerts?userId=1'"
+Write-Host "  curl 'http://localhost:3000/api/wallets?userId=user-123'"
+Write-Host "  curl 'http://localhost:3000/api/strategy'"
 Write-Host ""
 Read-Host "Appuyez sur Entree pour fermer cette fenetre"
diff --git a/Wallette/stop-all.bat b/Wallette/stop-all.bat
new file mode 100644 (file)
index 0000000..e8dca43
--- /dev/null
@@ -0,0 +1,39 @@
+@echo off
+REM =========================================================
+REM  WALL-E-TTE - Arrêt de tous les micro-services
+REM =========================================================
+
+echo [Wall-e-tte] Arret des services en cours...
+echo.
+
+REM ─── Tuer les process sur chaque port ────────────────────
+
+echo  Arret gateway     :3000
+FOR /F "tokens=5" %%P IN ('netstat -ano ^| findstr ":3000 " ^| findstr "LISTENING"') DO (
+    taskkill /PID %%P /F >nul 2>&1
+)
+
+echo  Arret price        :3001
+FOR /F "tokens=5" %%P IN ('netstat -ano ^| findstr ":3001 " ^| findstr "LISTENING"') DO (
+    taskkill /PID %%P /F >nul 2>&1
+)
+
+echo  Arret strategy     :3002
+FOR /F "tokens=5" %%P IN ('netstat -ano ^| findstr ":3002 " ^| findstr "LISTENING"') DO (
+    taskkill /PID %%P /F >nul 2>&1
+)
+
+echo  Arret alerts       :3003
+FOR /F "tokens=5" %%P IN ('netstat -ano ^| findstr ":3003 " ^| findstr "LISTENING"') DO (
+    taskkill /PID %%P /F >nul 2>&1
+)
+
+echo  Arret wallet       :3004
+FOR /F "tokens=5" %%P IN ('netstat -ano ^| findstr ":3004 " ^| findstr "LISTENING"') DO (
+    taskkill /PID %%P /F >nul 2>&1
+)
+
+echo.
+echo [Wall-e-tte] Tous les services sont arretes.
+echo.
+pause
index 2d57e272e91f7d9fd12b274a52e4ff06e97568d0..54012f4f5394d794d7d944576a400b797f608ffc 100644 (file)
@@ -13,8 +13,8 @@ import { io } from 'socket.io-client';
 // ───────────────────────────────────────────────────────────
 
 // L'adresse du serveur (à changer selon l'environnement)
-// En développement : localhost
-// En production : l'adresse du vrai serveur
+// On passe par le gateway :3000 qui proxifie le WebSocket
+// vers le alerts-service :3003 automatiquement
 const SERVER_URL = 'http://localhost:3000';
 
 // ───────────────────────────────────────────────────────────
@@ -31,28 +31,31 @@ let alertCallbacks = [];
 // FONCTION : Se connecter au serveur
 // ───────────────────────────────────────────────────────────
 // Appelle cette fonction quand l'utilisateur se connecte à ton site
-// 
+//
 // Paramètre : userId = l'identifiant de l'utilisateur connecté
 //
 // Exemple : connectToAlerts('user-123');
 // ───────────────────────────────────────────────────────────
 export function connectToAlerts(userId) {
-    
+
     // Si déjà connecté, on ne fait rien
     if (socket !== null) {
         console.log('⚠️ Déjà connecté au serveur d\'alertes');
         return;
     }
 
-    // Créer la connexion
-    socket = io(SERVER_URL);
+    // Créer la connexion via le gateway (qui proxifie /socket.io → :3003)
+    socket = io(SERVER_URL, {
+        path: '/socket.io',
+        transports: ['websocket', 'polling']
+    });
 
     // ─────────────────────────────────────────────────────
     // ÉVÉNEMENT : Connexion réussie
     // ─────────────────────────────────────────────────────
     socket.on('connect', function() {
         console.log('✅ Connecté au serveur d\'alertes');
-        
+
         // IMPORTANT : On envoie notre userId au serveur
         // Pour qu'il sache qui on est et nous envoie NOS alertes
         socket.emit('auth', userId);
@@ -70,7 +73,7 @@ export function connectToAlerts(userId) {
     // ─────────────────────────────────────────────────────
     socket.on('alert', function(alert) {
         console.log('🔔 ALERTE REÇUE :', alert);
-        
+
         // On appelle toutes les fonctions enregistrées
         // (voir onAlert plus bas)
         alertCallbacks.forEach(function(callback) {