diff --git a/src/app/(dashboard)/admin/synchronisation/client.tsx b/src/app/(dashboard)/admin/synchronisation/client.tsx
index 0f6c649..8171871 100644
--- a/src/app/(dashboard)/admin/synchronisation/client.tsx
+++ b/src/app/(dashboard)/admin/synchronisation/client.tsx
@@ -480,6 +480,8 @@ export function SynchronisationClient() {
Ce sont des chiffres mensuels : les refaire chaque nuit coûterait du temps pour rien.
+ Sont aussi refaits, sans attendre ce délai : les données d'avant le mois en cours
+ (le dernier mois leur manque) et une extraction en échec, retentée la nuit suivante.
diff --git a/src/features/admin/api/sync-scheduler.ts b/src/features/admin/api/sync-scheduler.ts
index b1b2e3b..7d80a44 100644
--- a/src/features/admin/api/sync-scheduler.ts
+++ b/src/features/admin/api/sync-scheduler.ts
@@ -140,19 +140,36 @@ async function prochainSql(depuis: Date) {
return row ?? null;
}
+/** Délai avant de retenter un fournisseur Qlik en échec : la nuit suivante. */
+const QLIK_REPRISE_ECHEC_MS = 18 * 3600 * 1000;
+
/**
- * Le plus ancien fournisseur Qlik dont les données dépassent `qlikMinJours`.
- * Le seuil en jours, plutôt que la fenêtre courante, évite de relancer chaque
- * nuit une extraction de plusieurs minutes pour des agrégats mensuels.
+ * Le plus ancien fournisseur Qlik à rafraîchir. Il l'est si :
+ * - ses données dépassent `qlikMinJours` (agrégats mensuels : inutile de
+ * relancer chaque nuit une extraction de plusieurs minutes) ;
+ * - ou elles datent d'avant le mois en cours : la fenêtre de 12 mois a glissé,
+ * son dernier mois manque, sa tendance n'est plus à jour ;
+ * - ou sa dernière extraction a échoué il y a plus de 18 h. Un échec attendait
+ * jusqu'ici `qlikMinJours` (7 jours) avant un nouvel essai, alors que la
+ * plupart sont passagers (serveur Qlik chargé, dernier mois pas encore
+ * chargé). Le plus ancien passant en premier, un fournisseur qui échoue à
+ * chaque fois repart en fin de file et ne bloque personne.
*/
async function prochainQlik(qlikMinJours: number, maintenant: Date) {
- const seuil = new Date(maintenant.getTime() - qlikMinJours * 24 * 3600 * 1000);
+ const seuilAge = new Date(maintenant.getTime() - qlikMinJours * 24 * 3600 * 1000);
+ const debutMois = new Date(maintenant.getFullYear(), maintenant.getMonth(), 1);
+ const seuil = seuilAge > debutMois ? seuilAge : debutMois;
+ const seuilEchec = new Date(maintenant.getTime() - QLIK_REPRISE_ECHEC_MS);
const [row] = await db
.select()
.from(syncFournisseurs)
.where(and(
eq(syncFournisseurs.actifQlik, true),
- or(isNull(syncFournisseurs.dernierQlikAt), lt(syncFournisseurs.dernierQlikAt, seuil)) as SQL,
+ or(
+ isNull(syncFournisseurs.dernierQlikAt),
+ lt(syncFournisseurs.dernierQlikAt, seuil),
+ and(eq(syncFournisseurs.dernierQlikStatut, "echec"), lt(syncFournisseurs.dernierQlikAt, seuilEchec)),
+ ) as SQL,
))
.orderBy(asc(sql`${syncFournisseurs.dernierQlikAt} nulls first`))
.limit(1);
diff --git a/src/lib/qlik-browser.ts b/src/lib/qlik-browser.ts
new file mode 100644
index 0000000..c91e360
--- /dev/null
+++ b/src/lib/qlik-browser.ts
@@ -0,0 +1,59 @@
+/**
+ * CollectFlow — Chromium et file d'attente partagés par tous les accès Qlik
+ * (extraction réseau `qlik-playwright.ts`, recherche produit `qlik-search.ts`).
+ */
+
+import "server-only";
+import { chromium, type Browser } from "playwright-core";
+
+let browserPromise: Promise | null = null;
+
+/**
+ * Chromium partagé entre les extractions.
+ *
+ * Un navigateur tombé (plantage, mémoire, arrêt du processus enfant) restait
+ * mémorisé : toutes les extractions suivantes échouaient sur « Target closed »
+ * jusqu'au redémarrage du serveur. On le relance donc dès qu'il est déconnecté.
+ */
+export async function getQlikBrowser(): Promise {
+ if (browserPromise) {
+ const existant = await browserPromise.catch(() => null);
+ if (existant?.isConnected()) return existant;
+ browserPromise = null;
+ }
+ const execPath = process.env.PLAYWRIGHT_CHROMIUM_PATH;
+ const lancement = chromium.launch({
+ headless: true,
+ executablePath: execPath || undefined,
+ args: ["--no-sandbox", "--disable-dev-shm-usage", "--ignore-certificate-errors"],
+ });
+ browserPromise = lancement;
+ lancement
+ .then((b) => b.on("disconnected", () => {
+ console.warn("[qlik] Chromium déconnecté — il sera relancé à la prochaine extraction");
+ if (browserPromise === lancement) browserPromise = null;
+ }))
+ .catch(() => { if (browserPromise === lancement) browserPromise = null; });
+ return lancement;
+}
+
+/**
+ * File unique des extractions.
+ *
+ * Le serveur Qlik abandonne les requêtes dès qu'il est trop sollicité. Une
+ * synchro manuelle lancée pendant la synchro de nuit, ou deux recherches
+ * produit simultanées, ouvraient deux sessions Engine en parallèle — et les
+ * deux échouaient. Les extractions passent désormais l'une après l'autre.
+ */
+let fileExtractions: Promise = Promise.resolve();
+let extractionsEnAttente = 0;
+
+export function dansLaFileQlik(libelle: string, tache: () => Promise): Promise {
+ if (extractionsEnAttente > 0) {
+ console.log(`[qlik] ${libelle} : en attente de ${extractionsEnAttente} extraction(s) en cours`);
+ }
+ extractionsEnAttente++;
+ const resultat = fileExtractions.then(tache, tache);
+ fileExtractions = resultat.then(() => undefined, () => undefined).finally(() => { extractionsEnAttente--; });
+ return resultat;
+}
diff --git a/src/lib/qlik-playwright.ts b/src/lib/qlik-playwright.ts
index e6d9a9d..3ac6926 100644
--- a/src/lib/qlik-playwright.ts
+++ b/src/lib/qlik-playwright.ts
@@ -13,23 +13,10 @@
*/
import "server-only";
-import { chromium, type Browser } from "playwright-core";
+import { getQlikBrowser as getBrowser, dansLaFileQlik as dansLaFile } from "@/lib/qlik-browser";
import { getQlikConfig, qlikNtlmSession, type QlikConfig, type NetworkMetric, type QlikMonthMetrics } from "@/lib/qlik-client";
import { buildGridNetworkQlikDateFilter, getMonthRanges, type QlikDateFilter } from "@/lib/qlik-date-range";
-
-let browserPromise: Promise | null = null;
-
-async function getBrowser(): Promise {
- if (!browserPromise) {
- const execPath = process.env.PLAYWRIGHT_CHROMIUM_PATH;
- browserPromise = chromium.launch({
- headless: true,
- executablePath: execPath || undefined,
- args: ["--no-sandbox", "--disable-dev-shm-usage", "--ignore-certificate-errors"],
- }).catch((e) => { browserPromise = null; throw e; });
- }
- return browserPromise;
-}
+import { assainirMetrique, controlerExtraction } from "@/lib/qlik-validation";
interface InPageResult {
ok: boolean;
@@ -86,7 +73,17 @@ function uniqueOrdered(values: number[]): number[] {
return out;
}
-export async function fetchNetworkMetricsPlaywright(
+export function fetchNetworkMetricsPlaywright(
+ codeCentraux?: string[],
+ cfg: QlikConfig = getQlikConfig(),
+ dateFilter: QlikDateFilter | null = buildGridNetworkQlikDateFilter(),
+ fournisseur?: string,
+): Promise