RELEASE V.4.0.0 : VERSION LONG SUPPORT (LTS)
--> Imports de ligne via fichier GTFS --> Extrapolation possible des données --> Générateur de fiches horaires
This commit is contained in:
1 parent
37ab831aac
commit
1c68740e41
25 files changed
+13372
-535
No files matched your search
+404
-51
@@ -1,105 +1,458 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const { spawn } = require('child_process');
|
||||
/**
|
||||
* Ferrovia Panel - Service de Mise à Jour Automatique & Gestionnaire Système
|
||||
*
|
||||
* Serveur autonome Node.js (avec ou sans express) pour orchestrer les opérations
|
||||
* de maintenance : git fetch/pull, npm install, compilation build et redémarrage PM2.
|
||||
*/
|
||||
|
||||
const http = require('http');
|
||||
const { spawn, execSync } = require('child_process');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
const PORT = parseInt(process.env.UPDATER_PORT || process.env.PORT || '4001', 10);
|
||||
const APP_DIR = path.resolve(__dirname);
|
||||
|
||||
const PORT = process.env.UPDATER_PORT || 4001;
|
||||
|
||||
// State to keep track of the current update process
|
||||
// État global du processus de mise à jour
|
||||
let updateState = {
|
||||
status: 'idle', // idle, updating, success, error
|
||||
status: 'idle', // 'idle' | 'checking' | 'updating' | 'success' | 'error'
|
||||
progress: 0,
|
||||
step: '',
|
||||
error: null,
|
||||
startedAt: null,
|
||||
finishedAt: null,
|
||||
logs: []
|
||||
};
|
||||
|
||||
function addLog(msg) {
|
||||
console.log(msg);
|
||||
updateState.logs.push(msg);
|
||||
if (updateState.logs.length > 100) {
|
||||
updateState.logs.shift(); // Keep last 100 logs
|
||||
/**
|
||||
* Ajout d'une ligne de journal horodatée avec typage
|
||||
*/
|
||||
function addLog(text, type = 'info') {
|
||||
const timestamp = new Date().toLocaleTimeString('fr-FR', { hour12: false });
|
||||
const logItem = {
|
||||
time: timestamp,
|
||||
text: String(text).trim(),
|
||||
type: type // 'info' | 'stdout' | 'stderr' | 'success' | 'error' | 'cmd'
|
||||
};
|
||||
|
||||
console.log(`[${timestamp}] [${type.toUpperCase()}] ${logItem.text}`);
|
||||
updateState.logs.push(logItem);
|
||||
|
||||
// Conserver les 300 derniers messages pour éviter la saturation mémoire
|
||||
if (updateState.logs.length > 300) {
|
||||
updateState.logs.shift();
|
||||
}
|
||||
}
|
||||
|
||||
function runCommand(command, args, stepName, progressStart, progressEnd) {
|
||||
/**
|
||||
* Exécute une commande shell de manière asynchrone avec streaming des logs
|
||||
*/
|
||||
function runCommand(command, args, stepName, progressStart, progressEnd, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
updateState.step = stepName;
|
||||
updateState.progress = progressStart;
|
||||
addLog(`>>> Exécution: ${command} ${args.join(' ')}`);
|
||||
addLog(`>>> Exécution: ${command} ${args.join(' ')}`, 'cmd');
|
||||
|
||||
const proc = spawn(command, args, { cwd: path.join(__dirname), shell: true });
|
||||
const proc = spawn(command, args, {
|
||||
cwd: APP_DIR,
|
||||
shell: true,
|
||||
env: { ...process.env, ...(options.env || {}) }
|
||||
});
|
||||
|
||||
proc.stdout.on('data', (data) => {
|
||||
addLog(data.toString().trim());
|
||||
const lines = data.toString().split('\n').filter(Boolean);
|
||||
lines.forEach(line => addLog(line, 'stdout'));
|
||||
});
|
||||
|
||||
proc.stderr.on('data', (data) => {
|
||||
addLog(`[ERREUR] ${data.toString().trim()}`);
|
||||
const lines = data.toString().split('\n').filter(Boolean);
|
||||
lines.forEach(line => {
|
||||
// Certains outils (npm, git) écrivent des infos normales sur stderr
|
||||
const isRealError = /error|fatal|fail/i.test(line);
|
||||
addLog(line, isRealError ? 'stderr' : 'info');
|
||||
});
|
||||
});
|
||||
|
||||
proc.on('error', (err) => {
|
||||
addLog(`Erreur de lancement de la commande: ${err.message}`, 'error');
|
||||
reject(err);
|
||||
});
|
||||
|
||||
proc.on('close', (code) => {
|
||||
if (code !== 0) {
|
||||
addLog(`>>> Échec avec le code ${code}`);
|
||||
reject(new Error(`La commande ${command} a échoué.`));
|
||||
const errMsg = `La commande "${command} ${args.join(' ')}" a échoué avec le code de sortie ${code}`;
|
||||
addLog(`>>> ${errMsg}`, 'error');
|
||||
reject(new Error(errMsg));
|
||||
} else {
|
||||
updateState.progress = progressEnd;
|
||||
addLog(`>>> Succès: ${stepName}`);
|
||||
addLog(`>>> Étape réussie : ${stepName}`, 'success');
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
app.get('/api/update/status', (req, res) => {
|
||||
res.json(updateState);
|
||||
});
|
||||
/**
|
||||
* Récupère les métadonnées de version et de dépôt local
|
||||
*/
|
||||
function getLocalSystemInfo() {
|
||||
let version = '0.1.0';
|
||||
try {
|
||||
const pkgJson = JSON.parse(fs.readFileSync(path.join(APP_DIR, 'package.json'), 'utf8'));
|
||||
version = pkgJson.version || version;
|
||||
} catch (e) {
|
||||
// Ignorer si package.json non lisible
|
||||
}
|
||||
|
||||
app.post('/api/update', async (req, res) => {
|
||||
let branch = 'main';
|
||||
let commitHash = '';
|
||||
let commitFullHash = '';
|
||||
let commitMessage = '';
|
||||
let commitDate = '';
|
||||
let commitAuthor = '';
|
||||
let remoteUrl = '';
|
||||
|
||||
try {
|
||||
branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: APP_DIR, encoding: 'utf8' }).trim();
|
||||
} catch (e) {
|
||||
branch = 'inconnu';
|
||||
}
|
||||
|
||||
try {
|
||||
commitHash = execSync('git rev-parse --short HEAD', { cwd: APP_DIR, encoding: 'utf8' }).trim();
|
||||
commitFullHash = execSync('git rev-parse HEAD', { cwd: APP_DIR, encoding: 'utf8' }).trim();
|
||||
commitMessage = execSync('git log -1 --pretty=%B', { cwd: APP_DIR, encoding: 'utf8' }).trim();
|
||||
commitDate = execSync('git log -1 --format=%cd --date=format:"%d/%m/%Y %H:%M:%S"', { cwd: APP_DIR, encoding: 'utf8' }).trim();
|
||||
commitAuthor = execSync('git log -1 --format=%an', { cwd: APP_DIR, encoding: 'utf8' }).trim();
|
||||
} catch (e) {
|
||||
// Ignorer si non disponible
|
||||
}
|
||||
|
||||
try {
|
||||
remoteUrl = execSync('git config --get remote.origin.url', { cwd: APP_DIR, encoding: 'utf8' }).trim();
|
||||
} catch (e) {
|
||||
remoteUrl = '';
|
||||
}
|
||||
|
||||
return {
|
||||
version,
|
||||
branch,
|
||||
commitHash,
|
||||
commitFullHash,
|
||||
commitMessage,
|
||||
commitDate,
|
||||
commitAuthor,
|
||||
remoteUrl,
|
||||
nodeVersion: process.version,
|
||||
platform: process.platform,
|
||||
pid: process.pid,
|
||||
uptime: Math.round(process.uptime())
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie l'état des mises à jour sur le dépôt Git distant
|
||||
*/
|
||||
async function checkGitUpdates() {
|
||||
try {
|
||||
// 1. Fetch silencieux
|
||||
execSync('git fetch origin', { cwd: APP_DIR, encoding: 'utf8', timeout: 15000 });
|
||||
|
||||
let branch = 'main';
|
||||
try {
|
||||
branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: APP_DIR, encoding: 'utf8' }).trim();
|
||||
} catch (e) {}
|
||||
|
||||
// 2. Compter les commits de retard
|
||||
let commitsBehind = 0;
|
||||
try {
|
||||
const behindCountStr = execSync(`git rev-list --count HEAD..origin/${branch}`, { cwd: APP_DIR, encoding: 'utf8' }).trim();
|
||||
commitsBehind = parseInt(behindCountStr, 10) || 0;
|
||||
} catch (e) {
|
||||
// Si pas de branche distante correspondante
|
||||
commitsBehind = 0;
|
||||
}
|
||||
|
||||
// 3. Récupérer la liste des commits entrants
|
||||
let incomingCommits = [];
|
||||
if (commitsBehind > 0) {
|
||||
try {
|
||||
const logOutput = execSync(`git log HEAD..origin/${branch} --format="%h|%an|%ar|%ad|%s" --date=format:"%d/%m/%Y %H:%M" -n 25`, {
|
||||
cwd: APP_DIR,
|
||||
encoding: 'utf8'
|
||||
}).trim();
|
||||
|
||||
if (logOutput) {
|
||||
incomingCommits = logOutput.split('\n').map(line => {
|
||||
const [hash, author, relativeDate, date, ...msgParts] = line.split('|');
|
||||
return {
|
||||
hash,
|
||||
author,
|
||||
relativeDate,
|
||||
date,
|
||||
message: msgParts.join('|')
|
||||
};
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore parsing errors
|
||||
}
|
||||
}
|
||||
|
||||
const localInfo = getLocalSystemInfo();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
hasUpdate: commitsBehind > 0,
|
||||
commitsBehind,
|
||||
branch,
|
||||
incomingCommits,
|
||||
localInfo,
|
||||
checkedAt: new Date().toISOString()
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
hasUpdate: false,
|
||||
error: `Impossible de contacter le dépôt distant : ${err.message}`,
|
||||
localInfo: getLocalSystemInfo(),
|
||||
checkedAt: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pipeline d'exécution de la mise à jour
|
||||
*/
|
||||
async function executeUpdatePipeline(options = {}) {
|
||||
if (updateState.status === 'updating') {
|
||||
return res.status(400).json({ error: 'Une mise à jour est déjà en cours.' });
|
||||
throw new Error('Une mise à jour est déjà en cours.');
|
||||
}
|
||||
|
||||
updateState = {
|
||||
status: 'updating',
|
||||
progress: 10,
|
||||
step: 'Initialisation',
|
||||
progress: 5,
|
||||
step: 'Initialisation de la mise à jour',
|
||||
error: null,
|
||||
startedAt: new Date().toISOString(),
|
||||
finishedAt: null,
|
||||
logs: []
|
||||
};
|
||||
|
||||
res.json({ message: 'Mise à jour démarrée' });
|
||||
|
||||
addLog('🚀 Démarrage du pipeline de mise à jour Ferrovia Panel...', 'info');
|
||||
addLog(`Répertoire applicatif : ${APP_DIR}`, 'info');
|
||||
|
||||
try {
|
||||
// Step 1: Git Pull (10% to 30%)
|
||||
await runCommand('git', ['pull'], 'Téléchargement des sources (git pull)', 10, 30);
|
||||
// Étape 1 : Récupération Git (5% -> 35%)
|
||||
let branch = 'main';
|
||||
try {
|
||||
branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: APP_DIR, encoding: 'utf8' }).trim();
|
||||
} catch (e) {}
|
||||
|
||||
await runCommand('git', ['fetch', 'origin'], 'Synchronisation des métadonnées Git', 10, 20);
|
||||
|
||||
// Si modifications locales non commitées, faire un stash temporaire sécurisé
|
||||
let hasStashed = false;
|
||||
try {
|
||||
const statusOutput = execSync('git status --porcelain', { cwd: APP_DIR, encoding: 'utf8' }).trim();
|
||||
if (statusOutput && options.stash !== false) {
|
||||
addLog('Modifications locales détectées, mise en réserve (git stash)...', 'info');
|
||||
execSync('git stash', { cwd: APP_DIR, encoding: 'utf8' });
|
||||
hasStashed = true;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
await runCommand('git', ['pull', 'origin', branch], `Téléchargement des dernières modifications (branche ${branch})`, 20, 40);
|
||||
|
||||
// Étape 2 : Installation des dépendances NPM (40% -> 65%)
|
||||
if (options.skipNpmInstall !== true) {
|
||||
await runCommand('npm', ['install', '--no-audit', '--no-fund'], 'Installation & mise à jour des dépendances (npm install)', 40, 65);
|
||||
} else {
|
||||
addLog('Étape npm install ignorée selon les paramètres.', 'info');
|
||||
updateState.progress = 65;
|
||||
}
|
||||
|
||||
// Étape 3 : Compilation de l'application React (65% -> 90%)
|
||||
if (options.skipBuild !== true) {
|
||||
await runCommand('npm', ['run', 'build'], 'Compilation de l\'application (npm run build)', 65, 90);
|
||||
} else {
|
||||
addLog('Étape de build ignorée selon les paramètres.', 'info');
|
||||
updateState.progress = 90;
|
||||
}
|
||||
|
||||
// Étape 4 : Détection PM2 & Redémarrage (90% -> 100%)
|
||||
updateState.step = 'Finalisation du déploiement';
|
||||
updateState.progress = 95;
|
||||
|
||||
// Step 2: NPM Install (30% to 60%)
|
||||
await runCommand('npm', ['install'], 'Installation des dépendances (npm install)', 30, 60);
|
||||
|
||||
// Step 3: Build (60% to 100%)
|
||||
await runCommand('npm', ['run', 'build'], 'Compilation de l\'application (npm run build)', 60, 100);
|
||||
let pm2Reloaded = false;
|
||||
if (options.restartPm2 !== false) {
|
||||
try {
|
||||
// Tester si PM2 est actif
|
||||
execSync('pm2 --version', { stdio: 'ignore' });
|
||||
addLog('PM2 détecté sur le système. Tentative de rechargement...', 'info');
|
||||
try {
|
||||
execSync('pm2 reload all || pm2 restart all', { cwd: APP_DIR, encoding: 'utf8', stdio: 'ignore' });
|
||||
addLog('Processus PM2 rechargés avec succès.', 'success');
|
||||
pm2Reloaded = true;
|
||||
} catch (e) {
|
||||
addLog(`Avertissement PM2 : ${e.message}`, 'info');
|
||||
}
|
||||
} catch (e) {
|
||||
addLog('PM2 non présent ou non configuré pour ce serveur.', 'info');
|
||||
}
|
||||
}
|
||||
|
||||
updateState.status = 'success';
|
||||
updateState.step = 'Terminé';
|
||||
updateState.step = 'Mise à jour terminée avec succès';
|
||||
updateState.progress = 100;
|
||||
addLog('>>> Mise à jour terminée avec succès.');
|
||||
|
||||
} catch (error) {
|
||||
updateState.finishedAt = new Date().toISOString();
|
||||
addLog('🎉 Félicitations ! Ferrovia Panel a été mis à jour avec succès.', 'success');
|
||||
|
||||
return { success: true, pm2Reloaded };
|
||||
} catch (err) {
|
||||
updateState.status = 'error';
|
||||
updateState.step = 'Erreur';
|
||||
addLog(`>>> ERREUR CRITIQUE: ${error.message}`);
|
||||
updateState.step = 'Échec de la mise à jour';
|
||||
updateState.error = err.message;
|
||||
updateState.finishedAt = new Date().toISOString();
|
||||
addLog(`❌ ERREUR CRITIQUE : ${err.message}`, 'error');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serveur HTTP natif Node.js avec gestion universelle des en-têtes CORS et JSON
|
||||
*/
|
||||
const server = http.createServer(async (req, res) => {
|
||||
// 1. En-têtes CORS complets
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');
|
||||
res.setHeader('Access-Control-Max-Age', '86400');
|
||||
|
||||
// Répondre aux requêtes preflight OPTIONS
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(204);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedUrl = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
||||
const pathname = parsedUrl.pathname;
|
||||
|
||||
const sendJson = (statusCode, data) => {
|
||||
res.writeHead(statusCode, { 'Content-Type': 'application/json; charset=utf-8' });
|
||||
res.end(JSON.stringify(data));
|
||||
};
|
||||
|
||||
const parseBody = () => {
|
||||
return new Promise((resolve) => {
|
||||
let body = '';
|
||||
req.on('data', chunk => { body += chunk; });
|
||||
req.on('end', () => {
|
||||
try {
|
||||
resolve(body ? JSON.parse(body) : {});
|
||||
} catch (e) {
|
||||
resolve({});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
// --- ROUTE: Health check ---
|
||||
if (pathname === '/api/health' || pathname === '/api/update/health') {
|
||||
return sendJson(200, {
|
||||
status: 'ok',
|
||||
service: 'ferrovia-updater',
|
||||
version: '2.0.0',
|
||||
uptime: Math.round(process.uptime()),
|
||||
pid: process.pid,
|
||||
port: PORT,
|
||||
appDir: APP_DIR
|
||||
});
|
||||
}
|
||||
|
||||
// --- ROUTE: Local System Information ---
|
||||
if (pathname === '/api/update/info' && req.method === 'GET') {
|
||||
const info = getLocalSystemInfo();
|
||||
return sendJson(200, info);
|
||||
}
|
||||
|
||||
// --- ROUTE: Check Remote Updates ---
|
||||
if ((pathname === '/api/update/check' || pathname === '/api/check-update') && req.method === 'GET') {
|
||||
const checkResult = await checkGitUpdates();
|
||||
return sendJson(200, checkResult);
|
||||
}
|
||||
|
||||
// --- ROUTE: Start Update Pipeline ---
|
||||
if ((pathname === '/api/update/start' || pathname === '/api/update') && req.method === 'POST') {
|
||||
if (updateState.status === 'updating') {
|
||||
return sendJson(409, {
|
||||
error: 'Une mise à jour est déjà en cours d\'exécution.',
|
||||
state: updateState
|
||||
});
|
||||
}
|
||||
|
||||
const body = await parseBody();
|
||||
|
||||
// Démarrage asynchrone pour répondre immédiatement au client
|
||||
executeUpdatePipeline(body).catch(err => {
|
||||
console.error('[UPDATER_ERROR]', err);
|
||||
});
|
||||
|
||||
return sendJson(202, {
|
||||
message: 'Le processus de mise à jour a été initialisé.',
|
||||
status: updateState.status,
|
||||
startedAt: updateState.startedAt
|
||||
});
|
||||
}
|
||||
|
||||
// --- ROUTE: Real-time Update Status ---
|
||||
if (pathname === '/api/update/status' && req.method === 'GET') {
|
||||
return sendJson(200, updateState);
|
||||
}
|
||||
|
||||
// --- ROUTE: Reset State ---
|
||||
if (pathname === '/api/update/reset' && req.method === 'POST') {
|
||||
if (updateState.status === 'updating') {
|
||||
return sendJson(400, { error: 'Impossible de réinitialiser pendant qu\'une mise à jour s\'exécute.' });
|
||||
}
|
||||
updateState = {
|
||||
status: 'idle',
|
||||
progress: 0,
|
||||
step: '',
|
||||
error: null,
|
||||
startedAt: null,
|
||||
finishedAt: null,
|
||||
logs: []
|
||||
};
|
||||
return sendJson(200, { message: 'État réinitialisé avec succès.', state: updateState });
|
||||
}
|
||||
|
||||
// 404 Route non trouvée
|
||||
return sendJson(404, { error: `Route introuvable : ${req.method} ${pathname}` });
|
||||
|
||||
} catch (err) {
|
||||
console.error('[UPDATER_SERVER_ERROR]', err);
|
||||
return sendJson(500, { error: err.message || 'Erreur interne du serveur de mise à jour' });
|
||||
}
|
||||
});
|
||||
|
||||
// Acknowledge update success and reset
|
||||
app.post('/api/update/reset', (req, res) => {
|
||||
updateState = { status: 'idle', progress: 0, step: '', logs: [] };
|
||||
res.json({ message: 'Reset OK' });
|
||||
server.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`====================================================`);
|
||||
console.log(`🚆 FERROVIA PANEL - UPDATER DAEMON DÉMARRÉ`);
|
||||
console.log(`📡 Port d'écoute : http://localhost:${PORT}`);
|
||||
console.log(`📁 Répertoire : ${APP_DIR}`);
|
||||
console.log(`====================================================`);
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`[Ferrovia Updater] Écoute sur le port ${PORT}`);
|
||||
// Arrêt propre
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('Fermeture du serveur updater (SIGTERM)...');
|
||||
server.close(() => process.exit(0));
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
console.log('Fermeture du serveur updater (SIGINT)...');
|
||||
server.close(() => process.exit(0));
|
||||
});
|
||||
Reference in new issue
Block a user