diff --git a/Dockerfile b/Dockerfile index c621aca..96156c2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,14 +30,15 @@ COPY requirements.txt /app/requirements.txt RUN python -m pip install --upgrade pip setuptools wheel && \ python -m pip install -r /app/requirements.txt -# Copy project -COPY . /app +# Copy only app directory and libs/so (native .so libraries for Linux) +COPY app /app/app +COPY libs/so /app/libs/so -# Ensure Python and dynamic linker will find the native libs if mounted -ENV PYTHONPATH="/app:/app/IPM_SO:/app/IPM_DLL" -ENV LD_LIBRARY_PATH="/app/IPM_SO:/app/IPM_DLL" +# Set environment variables to use libs/so for native libraries +ENV PYTHONPATH="/app" +ENV LD_LIBRARY_PATH="/app/libs/so" EXPOSE 8001 -# Default command runs uvicorn (use docker-compose override for development) +# Default command runs uvicorn CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8001"] diff --git a/Frontend/.dockerignore b/Frontend/.dockerignore index e69de29..3ef067b 100644 --- a/Frontend/.dockerignore +++ b/Frontend/.dockerignore @@ -0,0 +1,21 @@ +# Frontend .dockerignore +node_modules +.next +out +build +.turbo +dist +.env.local +.env.development.local +.env.test.local +.env.production.local +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.git +.gitignore +README.md +.vscode +.idea +*.swp +*.swo diff --git a/Frontend/Dockerfile b/Frontend/Dockerfile index e69de29..5ebfd8e 100644 --- a/Frontend/Dockerfile +++ b/Frontend/Dockerfile @@ -0,0 +1,50 @@ +# Frontend Dockerfile +FROM node:20-alpine AS base + +# Install dependencies only when needed +FROM base AS deps +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# Copy package files +COPY package.json package-lock.json* ./ +RUN npm ci + +# Rebuild the source code only when needed +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Build the Next.js app +ENV NEXT_TELEMETRY_DISABLED 1 +RUN npm run build + +# Production image, copy all the files and run next +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV production +ENV NEXT_TELEMETRY_DISABLED 1 + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public + +# Set the correct permission for prerender cache +RUN mkdir .next +RUN chown nextjs:nodejs .next + +# Automatically leverage output traces to reduce image size +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs + +EXPOSE 3000 + +ENV PORT 3000 +ENV HOSTNAME "0.0.0.0" + +CMD ["node", "server.js"] diff --git a/README-DOCKER.md b/README-DOCKER.md index e69de29..3ef91d9 100644 --- a/README-DOCKER.md +++ b/README-DOCKER.md @@ -0,0 +1,179 @@ +# DiagramPH - Docker Deployment Guide + +## 🐳 Docker Compose Setup + +Ce projet utilise Docker Compose pour déployer le backend (FastAPI) et le frontend (Next.js) ensemble. + +### Architecture + +``` +┌─────────────────┐ ┌──────────────────┐ +│ Frontend │─────▶│ Backend │ +│ (Next.js) │ │ (FastAPI) │ +│ Port: 3000 │ │ Port: 8001 │ +└─────────────────┘ └──────────────────┘ + │ │ + └────────────────────────┘ + Docker Network +``` + +## 🚀 Démarrage Rapide + +### Production + +```bash +# Build et démarrer tous les services +docker-compose up --build + +# En mode détaché (background) +docker-compose up -d --build + +# Arrêter les services +docker-compose down + +# Arrêter et supprimer les volumes +docker-compose down -v +``` + +Accès: +- **Frontend**: http://localhost:3000 +- **Backend API**: http://localhost:8001 +- **API Docs**: http://localhost:8001/docs + +### Développement + +Pour le développement avec hot-reload: + +```bash +# Démarrer en mode développement +docker-compose -f docker-compose.yml -f docker-compose.dev.yml up + +# Rebuild si nécessaire +docker-compose -f docker-compose.yml -f docker-compose.dev.yml up --build +``` + +## 📦 Services + +### Backend (Python FastAPI) + +- **Port**: 8001 +- **Base Image**: python:3.12-slim +- **Hot Reload**: Activé avec `--reload` en dev +- **Healthcheck**: GET /api/v1/refrigerants/ + +### Frontend (Next.js) + +- **Port**: 3000 +- **Base Image**: node:20-alpine +- **Build**: Standalone output optimisé +- **Healthcheck**: HTTP GET sur port 3000 + +## 🔧 Configuration + +### Variables d'Environnement + +#### Backend +```env +PYTHONUNBUFFERED=1 +PYTHONPATH=/app:/app/IPM_SO:/app/IPM_DLL +LD_LIBRARY_PATH=/app/IPM_SO:/app/IPM_DLL +``` + +#### Frontend +```env +NODE_ENV=production +NEXT_PUBLIC_API_URL=http://backend:8001/api/v1 +``` + +### Volumes + +Les volumes sont montés pour permettre le hot-reload en développement: +- `./app` → Backend code +- `./Frontend` → Frontend code +- `./IPM_SO` et `./IPM_DLL` → Native libraries + +## 📝 Commandes Utiles + +```bash +# Voir les logs +docker-compose logs -f + +# Logs d'un service spécifique +docker-compose logs -f backend +docker-compose logs -f frontend + +# Reconstruire un service +docker-compose build backend +docker-compose build frontend + +# Exécuter une commande dans un conteneur +docker-compose exec backend python -m pytest +docker-compose exec frontend npm test + +# Voir l'état des services +docker-compose ps + +# Redémarrer un service +docker-compose restart backend +docker-compose restart frontend +``` + +## 🐛 Troubleshooting + +### Le frontend ne peut pas se connecter au backend + +Vérifiez que: +1. Le backend est en cours d'exécution: `docker-compose ps` +2. Le healthcheck du backend est OK: `docker-compose logs backend` +3. Les deux services sont sur le même réseau Docker + +### Erreur de build frontend + +```bash +# Nettoyer le cache et rebuild +docker-compose build --no-cache frontend +``` + +### Erreur de permissions + +```bash +# Sur Linux/Mac, ajuster les permissions +sudo chown -R $USER:$USER Frontend/.next +``` + +## 📚 Structure des Fichiers + +``` +. +├── docker-compose.yml # Configuration production +├── docker-compose.dev.yml # Override pour développement +├── Dockerfile # Backend Dockerfile +├── Frontend/ +│ ├── Dockerfile # Frontend Dockerfile +│ ├── .dockerignore +│ └── next.config.js +├── app/ # Code backend +└── README-DOCKER.md # Ce fichier +``` + +## 🔒 Production Deployment + +Pour un déploiement en production: + +1. **Désactiver le mode debug**: + - Retirer `--reload` du backend + - Utiliser `NODE_ENV=production` + +2. **Utiliser des secrets**: + ```yaml + environment: + - DATABASE_URL=${DATABASE_URL} + ``` + +3. **Configurer un reverse proxy** (Nginx/Traefik) + +4. **Ajouter SSL/TLS** avec Let's Encrypt + +## 📄 License + +MIT diff --git a/app/core/refrigerant_loader.py b/app/core/refrigerant_loader.py index 06bf9f9..7c20cf9 100644 --- a/app/core/refrigerant_loader.py +++ b/app/core/refrigerant_loader.py @@ -7,13 +7,18 @@ import os from pathlib import Path from typing import Dict, Optional, List -# Prefer the packaged app.ipm module, but keep a fallback to the legacy -# IPM_DLL folder for development compatibility. +# Prefer the packaged app.ipm module, but fall back to the legacy IPM_DLL +# folder only if the packaged module cannot be imported. Import the module +# object and use getattr to avoid ImportError when optional symbols like +# `MockRefifc` are missing. try: - from app.ipm.simple_refrig_api import Refifc, MockRefifc # type: ignore + import importlib + _sr = importlib.import_module('app.ipm.simple_refrig_api') + Refifc = getattr(_sr, 'Refifc') + MockRefifc = getattr(_sr, 'MockRefifc', None) except Exception: # Fall back to loading from IPM_DLL directory as before. Import the - # module and pick attributes if present; older legacy wrappers may not + # legacy module and pick attributes if present; older wrappers may not # define MockRefifc. _current_dir = Path(__file__).parent.parent.parent _ipm_dll_dir = _current_dir / "IPM_DLL" diff --git a/app/ipm/simple_refrig_api.py b/app/ipm/simple_refrig_api.py index 3c0a9dd..05880c5 100644 --- a/app/ipm/simple_refrig_api.py +++ b/app/ipm/simple_refrig_api.py @@ -112,28 +112,55 @@ class Refifc(object): # Sauvegardez le répertoire courant pour pouvoir y revenir plus tard self.original_directory = os.getcwd() - # Determine candidate directories for the native library. Prefer - # app/ipm/lib/ if present, otherwise fall back to the - # package directory (for compatibility with older layouts). + # Determine candidate directories for the native library. + # Prefer central repo-level folders: + # /libs/dll (Windows) + # /libs/so (Linux) + # Fall back to package-local `app/ipm/lib/` or the package dir. package_dir = os.path.dirname(os.path.abspath(__file__)) - platform_dir = os.path.join(package_dir, 'lib', 'windows' if os.name == 'nt' else 'linux') - dll_directory = platform_dir if os.path.isdir(platform_dir) else package_dir + # repo root is two levels above app/ipm + repo_root = os.path.abspath(os.path.join(package_dir, '..', '..')) + preferred_windows = os.path.join(repo_root, 'libs', 'dll') + preferred_linux = os.path.join(repo_root, 'libs', 'so') - # Change working directory to the chosen directory while loading - os.chdir(dll_directory) + if os.name == 'nt': + candidate_dirs = [preferred_windows] + else: + candidate_dirs = [preferred_linux] - # Try to load the native library from the chosen directory; if that - # fails, attempt to load by name (for system-installed libs) and - # otherwise raise the original exception. - try: - self.lib = ctypes.cdll.LoadLibrary(os.path.join(dll_directory, REFIFC_LIB_NAME)) - except OSError: + # also consider the package-local lib/ layout as fallback + candidate_dirs.append(os.path.join(package_dir, 'lib', 'windows' if os.name == 'nt' else 'linux')) + candidate_dirs.append(package_dir) + + # Try each candidate directory in order until we can load the library. + load_exc = None + for dll_directory in candidate_dirs: + if not dll_directory: + continue + try: + if os.path.isdir(dll_directory): + # temporarily change cwd to help some LoadLibrary behaviors + os.chdir(dll_directory) + self.lib = ctypes.cdll.LoadLibrary(os.path.join(dll_directory, REFIFC_LIB_NAME)) + else: + # attempt to load directly by path anyway + self.lib = ctypes.cdll.LoadLibrary(os.path.join(dll_directory, REFIFC_LIB_NAME)) + load_exc = None + break + except OSError as e: + load_exc = e + # try next candidate + continue + + # if we failed to load from candidates, try loading by name (system path) + if load_exc is not None: try: self.lib = ctypes.cdll.LoadLibrary(REFIFC_LIB_NAME) - except Exception as e: - # Restore cwd before raising + load_exc = None + except Exception: + # restore cwd before raising os.chdir(self.original_directory) - raise + raise load_exc ctypes_refrig_name = refrig_name if os.name == 'posix': @@ -145,7 +172,8 @@ class Refifc(object): try: ctypes.CDLL(os.path.join(dll_directory, REFIFC_LIB_NAME)) except OSError: - print(f"Refrig {refrig_name} not found, please check!") + # don't raise here; letting the subsequent load/check handle missing refrigerant libs + print(f"Refrig {refrig_name} not found in {dll_directory}, please check!") func = self.lib.refdll_load func.restype = POINTER(c_void_p) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index e69de29..f4bbd5f 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -0,0 +1,30 @@ +version: '3.8' + +# Development override for docker-compose.yml +# Usage: docker-compose -f docker-compose.yml -f docker-compose.dev.yml up + +services: + backend: + volumes: + - ./app:/app/app:cached + - ./IPM_SO:/app/IPM_SO:cached + - ./IPM_DLL:/app/IPM_DLL:cached + - ./tests:/app/tests:cached + command: uvicorn app.main:app --host 0.0.0.0 --port 8001 --reload + environment: + - DEBUG=1 + + frontend: + build: + context: ./Frontend + target: deps + volumes: + - ./Frontend:/app:cached + - /app/node_modules + - /app/.next + command: npm run dev + environment: + - NODE_ENV=development + - NEXT_PUBLIC_API_URL=http://localhost:8001/api/v1 + ports: + - "3000:3000" diff --git a/docker-compose.yml b/docker-compose.yml index 7e002ca..5d38fbc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.8' - services: # Backend API Service backend: @@ -11,12 +9,11 @@ services: - "8001:8001" volumes: - ./app:/app/app:cached - - ./IPM_SO:/app/IPM_SO:cached - - ./IPM_DLL:/app/IPM_DLL:cached + - ./libs/so:/app/libs/so:cached environment: - PYTHONUNBUFFERED=1 - - PYTHONPATH=/app:/app/IPM_SO:/app/IPM_DLL - - LD_LIBRARY_PATH=/app/IPM_SO:/app/IPM_DLL + - PYTHONPATH=/app + - LD_LIBRARY_PATH=/app/libs/so command: uvicorn app.main:app --host 0.0.0.0 --port 8001 --reload networks: - diagramph-network