diff --git a/client/public/__manus__/version.json b/client/public/__manus__/version.json index 9eb04c1..0b4a46e 100644 --- a/client/public/__manus__/version.json +++ b/client/public/__manus__/version.json @@ -1,4 +1,4 @@ { - "version": "138ac2fc", - "timestamp": 1782484729381 + "version": "3967f6c2", + "timestamp": 1782655834971 } \ No newline at end of file diff --git a/client/src/pages/Settings.tsx b/client/src/pages/Settings.tsx index 7605dec..e436f35 100644 --- a/client/src/pages/Settings.tsx +++ b/client/src/pages/Settings.tsx @@ -220,6 +220,8 @@ export default function SettingsPage() { sharepoint_token: "", auth_mode: "local", import_time: "06:00", + fetch_mode: "scheduled", + fetch_interval_minutes: "1440", }); useEffect(() => { @@ -460,20 +462,83 @@ export default function SettingsPage() { Planification de l'import - L'import automatique s'exécute quotidiennement à l'heure configurée + Configurez la fréquence et l'heure de déclenchement des mises à jour automatiques (import Excel + lecture RSS). - -
- - set("import_time", e.target.value)} - /> -

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

+ + {/* Mode */} +
+ +
+ {(["scheduled", "interval"] as const).map((mode) => ( + + ))} +
+ + {/* Heure fixe */} + {(form.fetch_mode || "scheduled") === "scheduled" && ( +
+ + set("import_time", e.target.value)} + /> +

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

+
+ )} + + {/* Intervalle */} + {(form.fetch_mode || "scheduled") === "interval" && ( +
+ +
+ {[ + { 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) => ( + + ))} +
+

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

+
+ )}
diff --git a/server/_core/index.ts b/server/_core/index.ts index 0ddb5e0..94531cc 100644 --- a/server/_core/index.ts +++ b/server/_core/index.ts @@ -33,11 +33,29 @@ async function findAvailablePort(startPort: number = 3000): Promise { // ─── Tâche d'import quotidien + lecture RSS ────────────────────────────────── let cronJob: ReturnType | null = null; -async function scheduleDailyImport() { - // Heure configurable, défaut 06:00 +export async function scheduleDailyImport() { const importTime = (await getSetting("import_time")) || "06:00"; - const [hour, minute] = importTime.split(":").map(Number); - const cronExpr = `0 ${minute ?? 0} ${hour ?? 6} * * *`; + const fetchMode = (await getSetting("fetch_mode")) || "scheduled"; + const fetchIntervalMinutes = parseInt((await getSetting("fetch_interval_minutes")) || "1440", 10); + + let cronExpr: string; + if (fetchMode === "interval") { + const intervalMin = Math.max(60, fetchIntervalMinutes); + if (intervalMin < 60) { + cronExpr = `*/${intervalMin} * * * *`; + } else if (intervalMin % 60 === 0) { + const hours = intervalMin / 60; + cronExpr = hours === 24 ? `0 0 * * *` : `0 */${hours} * * *`; + } else { + cronExpr = `*/${intervalMin} * * * *`; + } + console.log(`[Cron] Mode intervalle — toutes les ${intervalMin} minutes (${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})`); + } + if (cronJob) { cronJob.stop(); cronJob = null; @@ -63,7 +81,6 @@ async function scheduleDailyImport() { console.error("[Cron] Erreur lors de la lecture RSS:", e); } }); - console.log(`[Cron] Import quotidien + lecture RSS planifiés à ${importTime} (${cronExpr})`); } /** diff --git a/server/routers.ts b/server/routers.ts index eb5c261..01a84c2 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -33,7 +33,7 @@ import { saveRssSettings, } from "./db"; import { importVeille, importAAP, runFullImport, getImportConfig } from "./importer"; -import { scheduleRssFetch } from "./_core/index"; +import { scheduleDailyImport } from "./_core/index"; import { loginLocalUser, hashPassword, ensureAdminExists } from "./localAuth"; import { classifyArticle } from "./aiClassifier"; import { getDb } from "./db"; @@ -372,6 +372,8 @@ 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_interval_minutes: z.string().optional(), }) ) .mutation(async ({ input }) => { @@ -380,6 +382,10 @@ export const appRouter = router({ if (v !== undefined && v !== "••••••••") toSave[k] = v; } await setSettings(toSave); + // Recharger le cron si la planification a changé + if (toSave.import_time || toSave.fetch_mode || toSave.fetch_interval_minutes) { + await scheduleDailyImport(); + } return { success: true }; }), }), @@ -592,8 +598,8 @@ export const appRouter = router({ })) .mutation(async ({ input }) => { await saveRssSettings(input); - // Recharger le planificateur RSS avec les nouveaux paramètres - await scheduleRssFetch(); + // Recharger le planificateur (la lecture RSS est intégrée dans scheduleDailyImport) + await scheduleDailyImport(); return { success: true }; }), }), diff --git a/todo.md b/todo.md index ede053a..3a6dddf 100644 --- a/todo.md +++ b/todo.md @@ -175,3 +175,10 @@ - [x] Couleur de sélection au clic sur une ligne (bg-primary/10) - [x] Filtre Lu/Non lu avec bouton segmenté (Non lu par défaut) - [x] Message vide adapté selon le filtre actif + +## Paramétrage fréquence des mises à jour +- [x] BDD : fetch_mode et fetch_interval_minutes stockés comme clés/valeurs dans app_settings (pas de migration nécessaire) +- [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) +- [ ] Déployer en recette