diff --git a/client/public/__manus__/version.json b/client/public/__manus__/version.json index 0b4a46e..e32a7a8 100644 --- a/client/public/__manus__/version.json +++ b/client/public/__manus__/version.json @@ -1,4 +1,4 @@ { - "version": "3967f6c2", - "timestamp": 1782655834971 + "version": "6c88c7af", + "timestamp": 1782767248272 } \ No newline at end of file diff --git a/client/src/pages/Settings.tsx b/client/src/pages/Settings.tsx index e436f35..b14b1e5 100644 --- a/client/src/pages/Settings.tsx +++ b/client/src/pages/Settings.tsx @@ -220,8 +220,10 @@ export default function SettingsPage() { sharepoint_token: "", auth_mode: "local", import_time: "06:00", - fetch_mode: "scheduled", + fetch_mode: "daily", fetch_interval_minutes: "1440", + fetch_week_day: "1", + fetch_month_day: "1", }); useEffect(() => { @@ -465,57 +467,106 @@ export default function SettingsPage() { Configurez la fréquence et l'heure de déclenchement des mises à jour automatiques (import Excel + lecture RSS). - {/* Mode */} + {/* Fréquence */}
- -
- {(["scheduled", "interval"] as const).map((mode) => ( + +
+ {([ + { value: "daily", label: "Quotidienne", desc: "Chaque jour à l'heure choisie" }, + { value: "weekly", label: "Hebdomadaire", desc: "Un jour précis chaque semaine" }, + { value: "monthly", label: "Mensuelle", desc: "Un jour précis chaque mois" }, + { value: "interval", label: "Intervalle", desc: "Toutes les N heures" }, + ] as const).map((opt) => ( ))}
- {/* Heure fixe */} - {(form.fetch_mode || "scheduled") === "scheduled" && ( + {/* Heure (tous les modes sauf intervalle) */} + {(form.fetch_mode || "daily") !== "interval" && (
- + set("import_time", e.target.value)} /> -

- Prochain import : demain à {form.import_time || "06:00"} -

+
+ )} + + {/* Jour de la semaine (hebdomadaire) */} + {(form.fetch_mode || "daily") === "weekly" && ( +
+ +
+ {[ + { label: "Lun", value: "1" }, + { label: "Mar", value: "2" }, + { label: "Mer", value: "3" }, + { label: "Jeu", value: "4" }, + { label: "Ven", value: "5" }, + { label: "Sam", value: "6" }, + { label: "Dim", value: "0" }, + ].map((d) => ( + + ))} +
+
+ )} + + {/* Jour du mois (mensuel) */} + {(form.fetch_mode || "daily") === "monthly" && ( +
+ + set("fetch_month_day", e.target.value)} + /> +

Maximum 28 pour garantir l'exécution tous les mois

)} {/* Intervalle */} - {(form.fetch_mode || "scheduled") === "interval" && ( -
- -
+ {(form.fetch_mode || "daily") === "interval" && ( +
+ +
{[ - { label: "1h", value: "60" }, - { label: "2h", value: "120" }, - { label: "4h", value: "240" }, - { label: "6h", value: "360" }, + { label: "1h", value: "60" }, + { label: "2h", value: "120" }, + { label: "4h", value: "240" }, + { label: "6h", value: "360" }, { label: "12h", value: "720" }, { label: "24h", value: "1440" }, ].map((opt) => ( @@ -524,7 +575,7 @@ export default function SettingsPage() { type="button" onClick={() => set("fetch_interval_minutes", opt.value)} className={cn( - "py-2 px-3 rounded-lg border-2 text-sm font-medium transition-all", + "w-14 py-2 rounded-lg border-2 text-sm font-medium transition-all", (form.fetch_interval_minutes || "1440") === opt.value ? "border-primary bg-primary/5 text-primary" : "border-border hover:border-primary/40 text-foreground" @@ -535,7 +586,9 @@ export default function SettingsPage() { ))}

- Import toutes les {(form.fetch_interval_minutes || "1440") === "60" ? "heure" : `${parseInt(form.fetch_interval_minutes || "1440") / 60} heures`} + Import toutes les {parseInt(form.fetch_interval_minutes || "1440") >= 60 + ? `${parseInt(form.fetch_interval_minutes || "1440") / 60}h` + : `${form.fetch_interval_minutes || "1440"} min`}

)} diff --git a/server/_core/index.ts b/server/_core/index.ts index 94531cc..ee3955a 100644 --- a/server/_core/index.ts +++ b/server/_core/index.ts @@ -35,27 +35,46 @@ let cronJob: ReturnType | null = null; export async function scheduleDailyImport() { const importTime = (await getSetting("import_time")) || "06:00"; - const fetchMode = (await getSetting("fetch_mode")) || "scheduled"; + const fetchMode = (await getSetting("fetch_mode")) || "daily"; const fetchIntervalMinutes = parseInt((await getSetting("fetch_interval_minutes")) || "1440", 10); + const fetchWeekDay = (await getSetting("fetch_week_day")) || "1"; // 0=dim, 1=lun, ..., 6=sam + const fetchMonthDay = (await getSetting("fetch_month_day")) || "1"; // 1-28 + + const [hour, minute] = importTime.split(":").map(Number); + const h = hour ?? 6; + const m = minute ?? 0; let cronExpr: string; + let cronLabel: string; + if (fetchMode === "interval") { const intervalMin = Math.max(60, fetchIntervalMinutes); - if (intervalMin < 60) { - cronExpr = `*/${intervalMin} * * * *`; - } else if (intervalMin % 60 === 0) { + if (intervalMin % 60 === 0) { const hours = intervalMin / 60; - cronExpr = hours === 24 ? `0 0 * * *` : `0 */${hours} * * *`; + cronExpr = hours === 24 ? `0 ${m} ${h} * * *` : `0 ${m} */${hours} * * *`; } else { - cronExpr = `*/${intervalMin} * * * *`; + cronExpr = `0 */${intervalMin} * * * *`; } - console.log(`[Cron] Mode intervalle — toutes les ${intervalMin} minutes (${cronExpr})`); + const hours = intervalMin / 60; + cronLabel = `Mode intervalle — toutes les ${hours >= 1 ? hours + "h" : intervalMin + "min"} (${cronExpr})`; + } else if (fetchMode === "weekly") { + // hebdomadaire : le jour choisi à l'heure choisie + cronExpr = `0 ${m} ${h} * * ${fetchWeekDay}`; + const jours = ["dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"]; + cronLabel = `Mode hebdomadaire — chaque ${jours[parseInt(fetchWeekDay)] ?? "lundi"} à ${importTime} (${cronExpr})`; + } else if (fetchMode === "monthly") { + // mensuel : le jour du mois choisi à l'heure choisie + const day = Math.min(28, Math.max(1, parseInt(fetchMonthDay))); + cronExpr = `0 ${m} ${h} ${day} * *`; + cronLabel = `Mode mensuel — le ${day} de chaque mois à ${importTime} (${cronExpr})`; } else { - const [hour, minute] = importTime.split(":").map(Number); - cronExpr = `0 ${minute ?? 0} ${hour ?? 6} * * *`; - console.log(`[Cron] Mode heure fixe — tous les jours à ${importTime} (${cronExpr})`); + // daily (défaut) + cronExpr = `0 ${m} ${h} * * *`; + cronLabel = `Mode quotidien — tous les jours à ${importTime} (${cronExpr})`; } + console.log(`[Cron] ${cronLabel}`); + if (cronJob) { cronJob.stop(); cronJob = null; diff --git a/server/routers.ts b/server/routers.ts index 01a84c2..5652515 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -372,8 +372,10 @@ export const appRouter = router({ sharepoint_token: z.string().optional(), auth_mode: z.enum(["local", "free"]).optional(), import_time: z.string().optional(), - fetch_mode: z.enum(["scheduled", "interval"]).optional(), + fetch_mode: z.enum(["daily", "weekly", "monthly", "interval", "scheduled"]).optional(), fetch_interval_minutes: z.string().optional(), + fetch_week_day: z.string().optional(), + fetch_month_day: z.string().optional(), }) ) .mutation(async ({ input }) => { @@ -383,7 +385,7 @@ export const appRouter = router({ } await setSettings(toSave); // Recharger le cron si la planification a changé - if (toSave.import_time || toSave.fetch_mode || toSave.fetch_interval_minutes) { + if (toSave.import_time || toSave.fetch_mode || toSave.fetch_interval_minutes || toSave.fetch_week_day || toSave.fetch_month_day) { await scheduleDailyImport(); } return { success: true }; diff --git a/todo.md b/todo.md index 3a6dddf..3666b0d 100644 --- a/todo.md +++ b/todo.md @@ -181,4 +181,9 @@ - [x] Backend index.ts : lire fetch_mode et fetch_interval_minutes pour construire l'expression cron - [x] Backend routers.ts : exposer et sauvegarder ces deux nouveaux paramètres - [x] Frontend Settings.tsx : sélecteur mode (heure fixe / intervalle) + sélecteur intervalle (1h, 2h, 4h, 6h, 12h, 24h) +- [x] Déployer en recette + +## Fréquence de planification enrichie +- [x] Backend index.ts : générer l'expression cron selon quotidien/hebdomadaire (jour)/mensuel/intervalle +- [x] Frontend Settings.tsx : sélecteur fréquence (quotidien, hebdo + jour, mensuel, intervalle) + heure - [ ] Déployer en recette