mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-12 01:36:43 +02:00
Page /admin/synchronisation : choisir quels fournisseurs sont cadencés, pour quelle source, et suivre l'avancement. ORDRE DE PASSAGE. La file est « le plus anciennement synchronisé d'abord », en ignorant ceux déjà traités depuis l'ouverture de la fenêtre. Il en découle, sans curseur ni compteur de tour : la nuit 2 reprend là où la nuit 1 s'est arrêtée, le tour recommence quand tout le monde est passé, et un fournisseur en échec garde une date ancienne donc repasse en tête. DEUX FILES. SQL (~43 s sur le plus gros) et Qlik (jusqu'à 8 min, sur un serveur qui abandonne quand on le sollicite trop) ont leur propre activation et leur propre rythme. Une extraction Qlik est intercalée tous les N fournisseurs SQL, sinon elle ne démarrerait jamais avant que la file SQL soit vide. Un plafond par nuit et un âge minimal évitent de refaire chaque nuit des agrégats mensuels. Détails qui comptent : - La fenêtre signifie « ne plus DÉMARRER après l'heure de fin » : une extraction en cours n'est jamais coupée. Le passage par minuit est géré. - L'horodatage est écrit même en cas d'échec, sans quoi le fournisseur fautif bloquerait la file derrière lui. - Garde anti-boucle : l'avancement repose sur cette écriture, volontairement non bloquante ; si elle échoue, le round s'arrête au lieu de reprendre le même fournisseur indéfiniment. - Un fournisseur sans article est désactivé automatiquement, motif affiché, réactivable — plutôt que réessayé chaque nuit. - Désactivé par défaut : un déploiement ne doit pas se mettre à solliciter Qlik la nuit suivante sans décision explicite. Déclenchement par minuteur interne (instrumentation.ts), battement d'une minute. Aucune configuration côté hôte, se réarme au démarrage ; en contrepartie rien ne tourne si l'application est arrêtée. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675
105 lines
3.8 KiB
TypeScript
105 lines
3.8 KiB
TypeScript
/**
|
|
* CollectFlow — Réglages de la synchronisation nocturne.
|
|
*
|
|
* Rangés dans `data/.db-config.json`, comme les autres réglages de
|
|
* l'application (base, Qlik, API FF), pour qu'il n'y ait qu'un seul endroit à
|
|
* sauvegarder et à monter dans le conteneur.
|
|
*
|
|
* Module **serveur**, pas fichier d'actions : il exporte aussi des valeurs
|
|
* (les défauts, le type), ce que `"use server"` interdit.
|
|
*/
|
|
|
|
import "server-only";
|
|
|
|
import fs from "fs/promises";
|
|
import path from "path";
|
|
|
|
const DATA_DIR = path.join(process.cwd(), "data");
|
|
const CONFIG_FILE = path.join(DATA_DIR, ".db-config.json");
|
|
|
|
export interface SyncSettings {
|
|
/**
|
|
* Désactivé par défaut : un déploiement ne doit pas se mettre à solliciter
|
|
* Qlik et la base la nuit suivante sans que personne l'ait décidé.
|
|
*/
|
|
actif: boolean;
|
|
/** Heure d'ouverture de la fenêtre (0-23). */
|
|
heureDebut: number;
|
|
/** Heure de fermeture. Aucun fournisseur n'est **démarré** après. */
|
|
heureFin: number;
|
|
/**
|
|
* Nombre maximum d'extractions Qlik par nuit. Chacune peut prendre 8 minutes
|
|
* et le serveur abandonne quand il est trop sollicité : ce plafond est la
|
|
* principale protection contre une nuit entièrement passée sur Qlik.
|
|
*/
|
|
qlikParNuit: number;
|
|
/** Une extraction Qlik est intercalée tous les N fournisseurs SQL. */
|
|
sqlParQlik: number;
|
|
/**
|
|
* Âge minimal des données Qlik avant de les refaire. Ce sont des agrégats
|
|
* mensuels : les rafraîchir toutes les nuits coûterait cher pour rien.
|
|
*/
|
|
qlikMinJours: number;
|
|
}
|
|
|
|
export const SYNC_SETTINGS_DEFAUT: SyncSettings = {
|
|
actif: false,
|
|
heureDebut: 0,
|
|
heureFin: 5,
|
|
qlikParNuit: 6,
|
|
sqlParQlik: 20,
|
|
qlikMinJours: 7,
|
|
};
|
|
|
|
function borne(v: unknown, min: number, max: number, defaut: number): number {
|
|
const n = Number(v);
|
|
if (!Number.isFinite(n)) return defaut;
|
|
return Math.min(max, Math.max(min, Math.trunc(n)));
|
|
}
|
|
|
|
/** Normalise ce qui vient du fichier : il est éditable à la main. */
|
|
function normaliser(brut: Partial<SyncSettings> | undefined): SyncSettings {
|
|
const d = SYNC_SETTINGS_DEFAUT;
|
|
const s: SyncSettings = {
|
|
actif: Boolean(brut?.actif),
|
|
heureDebut: borne(brut?.heureDebut, 0, 23, d.heureDebut),
|
|
heureFin: borne(brut?.heureFin, 0, 23, d.heureFin),
|
|
qlikParNuit: borne(brut?.qlikParNuit, 0, 200, d.qlikParNuit),
|
|
sqlParQlik: borne(brut?.sqlParQlik, 1, 500, d.sqlParQlik),
|
|
qlikMinJours: borne(brut?.qlikMinJours, 0, 365, d.qlikMinJours),
|
|
};
|
|
// Une fenêtre de durée nulle ne s'ouvrirait jamais : on revient au défaut
|
|
// plutôt que de laisser le planificateur muet sans explication.
|
|
if (s.heureDebut === s.heureFin) {
|
|
s.heureDebut = d.heureDebut;
|
|
s.heureFin = d.heureFin;
|
|
}
|
|
return s;
|
|
}
|
|
|
|
export async function readSyncSettings(): Promise<SyncSettings> {
|
|
try {
|
|
const raw = await fs.readFile(CONFIG_FILE, "utf-8");
|
|
const cfg = JSON.parse(raw) as { syncNuit?: Partial<SyncSettings> };
|
|
return normaliser(cfg?.syncNuit);
|
|
} catch {
|
|
return { ...SYNC_SETTINGS_DEFAUT };
|
|
}
|
|
}
|
|
|
|
export async function saveSyncSettings(patch: Partial<SyncSettings>): Promise<SyncSettings> {
|
|
let existant: Record<string, unknown> = {};
|
|
try {
|
|
existant = JSON.parse(await fs.readFile(CONFIG_FILE, "utf-8"));
|
|
} catch {
|
|
// Pas encore de fichier : on le crée avec les seuls réglages de synchro.
|
|
}
|
|
const courant = normaliser((existant.syncNuit as Partial<SyncSettings>) ?? undefined);
|
|
const suivant = normaliser({ ...courant, ...patch });
|
|
|
|
await fs.mkdir(DATA_DIR, { recursive: true });
|
|
await fs.writeFile(CONFIG_FILE, JSON.stringify({ ...existant, syncNuit: suivant }, null, 2));
|
|
console.log(`[sync-nuit] réglages enregistrés : ${JSON.stringify(suivant)}`);
|
|
return suivant;
|
|
}
|