Checkpoint: Ajout du paramétrage de fréquence des mises à jour dans Paramètres → Import & Planification : choix entre mode heure fixe (cron quotidien) et mode intervalle (1h, 2h, 4h, 6h, 12h, 24h). Le cron est rechargé immédiatement après sauvegarde sans redémarrage serveur.

This commit is contained in:
Manus
2026-06-28 10:10:34 -04:00
parent 9eb5849504
commit 1f1a479029
5 changed files with 117 additions and 22 deletions

View File

@@ -1,4 +1,4 @@
{
"version": "138ac2fc",
"timestamp": 1782484729381
"version": "3967f6c2",
"timestamp": 1782655834971
}

View File

@@ -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,9 +462,37 @@ export default function SettingsPage() {
<Card>
<CardHeader>
<CardTitle className="text-base flex items-center gap-2"><Clock size={16} />Planification de l'import</CardTitle>
<CardDescription>L'import automatique s'exécute quotidiennement à l'heure configurée</CardDescription>
<CardDescription>Configurez la fréquence et l'heure de déclenchement des mises à jour automatiques (import Excel + lecture RSS).</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<CardContent className="space-y-5">
{/* Mode */}
<div className="space-y-2">
<Label className="text-sm font-medium">Mode de planification</Label>
<div className="grid grid-cols-2 gap-3 max-w-md">
{(["scheduled", "interval"] as const).map((mode) => (
<button
key={mode}
type="button"
onClick={() => set("fetch_mode", mode)}
className={cn(
"flex items-center gap-2 p-3 rounded-lg border-2 text-left transition-all text-sm",
(form.fetch_mode || "scheduled") === mode
? "border-primary bg-primary/5"
: "border-border hover:border-primary/40"
)}
>
<Clock className={cn("h-4 w-4", (form.fetch_mode || "scheduled") === mode ? "text-primary" : "text-muted-foreground")} />
<div>
<p className="font-medium">{mode === "scheduled" ? "Heure fixe" : "Intervalle"}</p>
<p className="text-xs text-muted-foreground">{mode === "scheduled" ? "Chaque jour à une heure précise" : "Toutes les N heures"}</p>
</div>
</button>
))}
</div>
</div>
{/* Heure fixe */}
{(form.fetch_mode || "scheduled") === "scheduled" && (
<div className="space-y-2 max-w-xs">
<Label>Heure d'import quotidien</Label>
<Input
@@ -474,6 +504,41 @@ export default function SettingsPage() {
Prochain import : demain à {form.import_time || "06:00"}
</p>
</div>
)}
{/* Intervalle */}
{(form.fetch_mode || "scheduled") === "interval" && (
<div className="space-y-2 max-w-xs">
<Label>Fréquence de mise à jour</Label>
<div className="grid grid-cols-3 gap-2">
{[
{ 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) => (
<button
key={opt.value}
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",
(form.fetch_interval_minutes || "1440") === opt.value
? "border-primary bg-primary/5 text-primary"
: "border-border hover:border-primary/40 text-foreground"
)}
>
{opt.label}
</button>
))}
</div>
<p className="text-xs text-muted-foreground">
Import toutes les {(form.fetch_interval_minutes || "1440") === "60" ? "heure" : `${parseInt(form.fetch_interval_minutes || "1440") / 60} heures`}
</p>
</div>
)}
</CardContent>
</Card>

View File

@@ -33,11 +33,29 @@ async function findAvailablePort(startPort: number = 3000): Promise<number> {
// ─── Tâche d'import quotidien + lecture RSS ──────────────────────────────────
let cronJob: ReturnType<typeof cron.schedule> | null = null;
async function scheduleDailyImport() {
// Heure configurable, défaut 06:00
export async function scheduleDailyImport() {
const importTime = (await getSetting("import_time")) || "06:00";
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);
const cronExpr = `0 ${minute ?? 0} ${hour ?? 6} * * *`;
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})`);
}
/**

View File

@@ -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 };
}),
}),

View File

@@ -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