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

+30 -4
View File
@@ -3,7 +3,7 @@ import { promisify } from 'util';
import fs from 'fs';
import path from 'path';
import { nanoid } from 'nanoid';
import { eq, desc } from "drizzle-orm";
import { eq, desc, sql } from "drizzle-orm";
import { db } from "./db";
import { databaseBackups, utilities, users } from "@shared/schema";
import type { DatabaseBackup, InsertDatabaseBackup } from "@shared/schema";
@@ -14,6 +14,8 @@ export class BackupService {
private backupDir: string;
private maxBackups = 10;
private lastAutomaticBackupDate: string | null = null;
// Vérification quotidienne en cours, partagée par les connexions simultanées
private dailyBackupCheck: Promise<{ backupPerformed: boolean; message: string }> | null = null;
constructor() {
// Use /app/backups in production (with proper permissions), or use env variable
@@ -130,9 +132,10 @@ export class BackupService {
await execAsync(command, { env });
// Get file stats and count tables
const stats = fs.statSync(filepath);
const sqlContent = fs.readFileSync(filepath, 'utf8');
const tablesCount = (sqlContent.match(/CREATE TABLE/g) || []).length;
// Tables comptées en base plutôt qu'en relisant tout le dump en mémoire :
// la lecture synchrone et la regex gelaient le serveur sur les grosses bases
const stats = await fs.promises.stat(filepath);
const tablesCount = await this.countDatabaseTables();
// Update database record with completion details
const [updatedBackup] = await db.update(databaseBackups)
@@ -156,6 +159,18 @@ export class BackupService {
}
}
// Nombre de tables utilisateur (hors schémas système), proche de ce que
// pg_dump inclut dans la sauvegarde
private async countDatabaseTables(): Promise<number> {
const result = await db.execute(sql`
SELECT count(*)::int AS count
FROM information_schema.tables
WHERE table_type = 'BASE TABLE'
AND table_schema NOT IN ('pg_catalog', 'information_schema')
`);
return Number(result.rows[0]?.count ?? 0);
}
async getBackupList(): Promise<DatabaseBackup[]> {
try {
const backups = await db.select()
@@ -238,6 +253,17 @@ export class BackupService {
// Nouvelle méthode : Vérifier et effectuer une sauvegarde quotidienne si nécessaire
async checkAndPerformDailyBackup(userId: string = 'system'): Promise<{ backupPerformed: boolean; message: string }> {
// Plusieurs connexions simultanées (ouverture des magasins) partagent la
// même vérification : un seul pg_dump est lancé
if (!this.dailyBackupCheck) {
this.dailyBackupCheck = this.performDailyBackupCheck(userId).finally(() => {
this.dailyBackupCheck = null;
});
}
return this.dailyBackupCheck;
}
private async performDailyBackupCheck(userId: string): Promise<{ backupPerformed: boolean; message: string }> {
try {
// Vérifier si les backups automatiques sont activés
const [config] = await db.select()