diff --git a/.gitea/workflows/validate.yml b/.gitea/workflows/validate.yml index 501d730..7d0f583 100644 --- a/.gitea/workflows/validate.yml +++ b/.gitea/workflows/validate.yml @@ -22,4 +22,5 @@ jobs: working-directory: src/frontend run: | npm ci + npm test npm run build diff --git a/docker-compose.yml b/docker-compose.yml index 1de61c9..a454dd0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,17 +9,22 @@ services: # Le dashboard doit repartir seul après un arrêt anormal du processus. restart: always mem_limit: 512m + env_file: + - .env environment: - - NODE_ENV=production - - DASHBOARD_ENV=recette - - PORT=3001 - - JWT_SECRET=manus-dashboard-jwt-secret-2026-recette - - ADMIN_USERNAME=adminItinova - - ADMIN_PASSWORD=Itinova69! - - GITEA_URL=https://git.recette.santinova-soft.org - - GITEA_USERNAME=manus-admin + - NODE_ENV=${NODE_ENV:-production} + - DASHBOARD_ENV=${DASHBOARD_ENV:-recette} + - PORT=${PORT:-3001} + - JWT_SECRET=${JWT_SECRET:?JWT_SECRET requis dans le fichier .env serveur} + - ADMIN_USERNAME=${ADMIN_USERNAME:?ADMIN_USERNAME requis dans le fichier .env serveur} + - ADMIN_PASSWORD=${ADMIN_PASSWORD:?ADMIN_PASSWORD requis dans le fichier .env serveur} + - GITEA_URL=${GITEA_URL:?GITEA_URL requis dans le fichier .env serveur} + - GITEA_USERNAME=${GITEA_USERNAME:-manus-admin} - GITEA_TOKEN=${GITEA_TOKEN:?GITEA_TOKEN requis dans le fichier .env serveur} - - GITEA_RECETTE_URL=http://gitea:3000 + - GITEA_RECETTE_URL=${GITEA_RECETTE_URL:-http://gitea:3000} + - GITEA_RECETTE_PUBLIC_URL=${GITEA_RECETTE_PUBLIC_URL:-https://git.recette.santinova-soft.org} + - GITEA_RECETTE_TOKEN=${GITEA_RECETTE_TOKEN:-} + - GITEA_PROD_URL=${GITEA_PROD_URL:-https://git.santinova-soft.org} - GITEA_TOKEN_PROD=${GITEA_TOKEN_PROD:-} - WEBHOOK_SECRET=${WEBHOOK_SECRET:?WEBHOOK_SECRET requis dans le fichier .env serveur} - IA_REFERENTIEL_URL=${IA_REFERENTIEL_URL:-https://referentiel-ia.recette.santinova-soft.org} @@ -35,12 +40,13 @@ services: - /opt/manus-deploy:/opt/manus-deploy extra_hosts: - "git.santinova-soft.org:180.149.196.138" + - "git.recette.santinova-soft.org:78.138.58.109" networks: - web labels: - "traefik.enable=true" - # Route HTTPS via dashboard.recette.santinova-soft.org - - "traefik.http.routers.manus-dashboard.rule=Host(`dashboard.recette.santinova-soft.org`)" + # Le domaine public est spécifique au serveur, mais le commit reste commun. + - "traefik.http.routers.manus-dashboard.rule=Host(`${DASHBOARD_DOMAIN:?DASHBOARD_DOMAIN requis dans le fichier .env serveur}`)" - "traefik.http.routers.manus-dashboard.entrypoints=websecure" - "traefik.http.routers.manus-dashboard.tls=true" - "traefik.http.routers.manus-dashboard.tls.certresolver=letsencrypt" diff --git a/src/frontend/index.html b/src/frontend/index.html index 4b7f321..2cbebf5 100644 --- a/src/frontend/index.html +++ b/src/frontend/index.html @@ -4,7 +4,7 @@ -
- Déployez une nouvelle application sur le serveur de recette + Déployez une nouvelle application sur le serveur de {ENVIRONMENT_LABEL_LOWER}
- .recette.santinova-soft.org + .{APP_DOMAIN_SUFFIX} {errors.subdomain && ( @@ -280,7 +281,7 @@ export default function AddAppModal({ isOpen, onClose, onSuccess }) { {formData.subdomain && !errors.subdomain && (
- URL: https://{formData.subdomain}.recette.santinova-soft.org + URL: https://{formData.subdomain}.{APP_DOMAIN_SUFFIX}
) : ( @@ -573,7 +574,7 @@ export default function AddAppModal({ isOpen, onClose, onSuccess }) {Recette
+{ENVIRONMENT_LABEL}
Explorez les dépôts sur{' '} - git.recette.santinova-soft.org + {GITEA_HOST}
Gestion des applications
- Santinova Soft — Serveur de recette + Santinova Soft — Serveur de {ENVIRONMENT_LABEL_LOWER}
diff --git a/src/frontend/src/pages/TerminalPage.jsx b/src/frontend/src/pages/TerminalPage.jsx index d1cf1d4..d1a9e40 100644 --- a/src/frontend/src/pages/TerminalPage.jsx +++ b/src/frontend/src/pages/TerminalPage.jsx @@ -1,5 +1,6 @@ import React, { useState, useEffect, useRef, useCallback } from 'react'; import { Terminal, Wifi, WifiOff, X, Plus, ChevronDown } from 'lucide-react'; +import { DEFAULT_SSH_PRESET, ENVIRONMENT_LABEL, SERVER_PUBLIC_IP } from '../utils/environment'; // Serveurs SSH prédéfinis const PRESET_SERVERS = [ @@ -30,8 +31,8 @@ export default function TerminalPage() { // Formulaire de connexion const [form, setForm] = useState({ - preset: 'recette', - host: '78.138.58.109', + preset: DEFAULT_SSH_PRESET, + host: SERVER_PUBLIC_IP, port: 22, username: 'root', password: '', @@ -116,7 +117,7 @@ export default function TerminalPage() { fitAddonRef.current = fitAddon; term.writeln('\x1b[1;34m╔══════════════════════════════════════════════════╗\x1b[0m'); - term.writeln('\x1b[1;34m║ Terminal SSH — Dashboard Recette Santinova ║\x1b[0m'); + term.writeln(`\x1b[1;34m║ Terminal SSH — Dashboard ${ENVIRONMENT_LABEL} Santinova ║\x1b[0m`); term.writeln('\x1b[1;34m╚══════════════════════════════════════════════════╝\x1b[0m'); term.writeln(''); term.writeln('\x1b[90mSélectionnez un serveur et connectez-vous pour démarrer.\x1b[0m'); diff --git a/src/frontend/src/utils/environment.js b/src/frontend/src/utils/environment.js new file mode 100644 index 0000000..18975c9 --- /dev/null +++ b/src/frontend/src/utils/environment.js @@ -0,0 +1,39 @@ +/** + * Identité de l'environnement courant, déterminée par le domaine public. + * Le même artefact frontend peut ainsi être construit et exécuté en recette + * comme en production sans branche ni modification de code spécifique. + */ +export function getEnvironmentConfig(hostname) { + const normalizedHostname = hostname.toLowerCase(); + const isRecette = + normalizedHostname.includes('recette') || + normalizedHostname === 'localhost' || + normalizedHostname === '127.0.0.1'; + + return { + isRecette, + label: isRecette ? 'Recette' : 'Production', + labelLower: isRecette ? 'recette' : 'production', + serverPublicIp: isRecette ? '78.138.58.109' : '180.149.196.138', + giteaUrl: isRecette + ? 'https://git.recette.santinova-soft.org' + : 'https://git.santinova-soft.org', + appDomainSuffix: isRecette + ? 'recette.santinova-soft.org' + : 'santinova-soft.org', + defaultSshPreset: isRecette ? 'recette' : 'production', + }; +} + +const currentHostname = typeof window === 'undefined' ? 'localhost' : window.location.hostname; +const environment = getEnvironmentConfig(currentHostname); + +export const IS_RECETTE = environment.isRecette; +export const ENVIRONMENT_LABEL = environment.label; +export const ENVIRONMENT_LABEL_LOWER = environment.labelLower; +export const SERVER_PUBLIC_IP = environment.serverPublicIp; +export const GITEA_URL = environment.giteaUrl; +export const GITEA_HOST = new URL(GITEA_URL).hostname; +export const APP_DOMAIN_SUFFIX = environment.appDomainSuffix; +export const DEFAULT_SSH_PRESET = environment.defaultSshPreset; + diff --git a/src/frontend/test/environment.test.mjs b/src/frontend/test/environment.test.mjs new file mode 100644 index 0000000..4e149ec --- /dev/null +++ b/src/frontend/test/environment.test.mjs @@ -0,0 +1,27 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { getEnvironmentConfig } from '../src/utils/environment.js'; + +test('détecte le dashboard de recette depuis son domaine public', () => { + const config = getEnvironmentConfig('dashboard.recette.santinova-soft.org'); + assert.equal(config.isRecette, true); + assert.equal(config.label, 'Recette'); + assert.equal(config.serverPublicIp, '78.138.58.109'); + assert.equal(config.giteaUrl, 'https://git.recette.santinova-soft.org'); + assert.equal(config.appDomainSuffix, 'recette.santinova-soft.org'); +}); + +test('détecte le dashboard de production depuis son domaine public', () => { + const config = getEnvironmentConfig('dashboard.santinova-soft.org'); + assert.equal(config.isRecette, false); + assert.equal(config.label, 'Production'); + assert.equal(config.serverPublicIp, '180.149.196.138'); + assert.equal(config.giteaUrl, 'https://git.santinova-soft.org'); + assert.equal(config.appDomainSuffix, 'santinova-soft.org'); +}); + +test('utilise la recette pour le développement local', () => { + assert.equal(getEnvironmentConfig('localhost').isRecette, true); + assert.equal(getEnvironmentConfig('127.0.0.1').isRecette, true); +});