perf(serveur): réponses API allégées, compression, cache des assets et index

- Fichiers statiques servis avant la session (plus de requêtes SQL par
  asset), /assets en cache immuable 1 an, index.html en no-cache, et
  compression gzip/brotli des réponses (dépendance compression, externe
  dans le bundle esbuild du Dockerfile).
- req.user (déjà chargé par deserializeUser) réutilisé dans les handlers
  au lieu de relire l'utilisateur et ses magasins à chaque appel ;
  GET /api/user ne refait plus de requête.
- Listes : le magasin joint est réduit aux champs lus par l'interface
  (plus de logo base64 ni de configuration SMTP/NocoDB dans chaque ligne),
  plus aucune empreinte de mot de passe dans les créateurs/auteurs ni dans
  /api/users.
- N+1 supprimés (/api/users, annonces, historique SAV, caches de
  vérification des factures), requêtes indépendantes en parallèle (stats,
  analytics, météo, getDelivery, getUserWithGroups), jointure
  multiplicative des statistiques par magasin corrigée.
- Échéancier limité au magasin demandé ; filtre status sur
  GET /api/deliveries.
- Index de performance créés en arrière-plan au démarrage
  (CREATE INDEX CONCURRENTLY, reliquats invalides purgés sans verrou
  exclusif).
- La connexion n'attend plus la sauvegarde quotidienne ; purge du cache
  des factures active en production ; logs volumineux retirés des
  chemins chauds ; NODE_ENV fixé dans l'image.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MsDJjQrAggcJwbbBtKhgyb
This commit is contained in:
Claude committed 2026-10-03 04:29:51 +00:00
1 parent 3f2660c506
commit a5785f0244
13 files changed
+1113 -1116

No files matched your search

+44 -25
View File
@@ -1,4 +1,5 @@
import express, { type Express, type Request, type Response, type NextFunction } from "express";
import compression from "compression";
import { createServer, type Server } from "http";
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
@@ -24,6 +25,10 @@ console.log('🐳 Environment:', {
const app = express();
// Compression gzip des réponses (assets JS/CSS, index.html et JSON de l'API).
// En premier pour couvrir toutes les réponses.
app.use(compression());
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: false, limit: '10mb' }));
@@ -126,48 +131,62 @@ async function registerProductionRoutes(app: Express): Promise<void> {
});
}
await registerProductionRoutes(app);
app.use((err: any, _req: Request, res: Response, _next: NextFunction) => {
const status = err.status || err.statusCode || 500;
const message = err.message || "Internal Server Error";
console.error('Server error:', { status, message, error: err });
res.status(status).json({ message });
throw err;
});
// Serve static files directly in production (no Vite)
// Déclarés AVANT les routes : les fichiers statiques et l'index.html ne passent
// ni par la session ni par passport (aucune requête SQL par fichier).
const publicPath = join(__dirname, 'public');
console.log('🐳 Serving static files from:', publicPath);
app.use('/assets', express.static(join(publicPath, 'assets')));
app.use('/', express.static(publicPath));
// Add explicit root route handler
app.get('/', (req, res) => {
console.log('🏠 ROOT: Serving index.html for root request');
res.sendFile(join(publicPath, 'index.html'), (err) => {
if (err) {
console.error('❌ ROOT: Error serving index.html:', err);
res.status(500).send('Error loading application');
// Les fichiers de /assets ont un nom haché par Vite : cache navigateur d'un an
app.use('/assets', express.static(join(publicPath, 'assets'), {
maxAge: '1y',
immutable: true,
index: false,
}));
// Autres fichiers publics (favicon, manifest...). L'index.html est toujours
// revalidé pour qu'un nouveau déploiement soit pris en compte immédiatement.
app.use(express.static(publicPath, {
index: false,
setHeaders: (res, filePath) => {
if (filePath.endsWith('.html')) {
res.setHeader('Cache-Control', 'no-cache');
}
});
});
},
}));
// SPA fallback - serve index.html for all non-API routes
// SPA fallback - serve index.html for all non-API routes (y compris "/")
app.get('*', (req, res, next) => {
if (req.path.startsWith('/api/')) {
return next();
}
console.log(`📄 SPA: Serving index.html for ${req.path}`);
res.set('Cache-Control', 'no-cache');
res.sendFile(join(publicPath, 'index.html'), (err) => {
if (err) {
console.error(`❌ SPA: Error serving index.html for ${req.path}:`, err);
res.status(500).send('Error loading application');
if (!res.headersSent) {
res.status(500).send('Error loading application');
}
}
});
});
await registerProductionRoutes(app);
// Tâches de maintenance périodiques (purge du cache factures expiré)
const { startMaintenanceJobs } = await import('./maintenance.js');
startMaintenanceJobs();
app.use((err: any, _req: Request, res: Response, next: NextFunction) => {
// Réponse déjà commencée : on laisse Express clore la connexion
if (res.headersSent) {
return next(err);
}
const status = err.status || err.statusCode || 500;
const message = err.message || "Internal Server Error";
console.error('Server error:', { status, message, error: err });
res.status(status).json({ message });
});
const port = process.env.PORT ? parseInt(process.env.PORT) : 3000;
const server = createServer(app);
server.listen(port, "0.0.0.0", () => {