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:
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"version": "138ac2fc",
|
"version": "3967f6c2",
|
||||||
"timestamp": 1782484729381
|
"timestamp": 1782655834971
|
||||||
}
|
}
|
||||||
@@ -220,6 +220,8 @@ export default function SettingsPage() {
|
|||||||
sharepoint_token: "",
|
sharepoint_token: "",
|
||||||
auth_mode: "local",
|
auth_mode: "local",
|
||||||
import_time: "06:00",
|
import_time: "06:00",
|
||||||
|
fetch_mode: "scheduled",
|
||||||
|
fetch_interval_minutes: "1440",
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -460,9 +462,37 @@ export default function SettingsPage() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-base flex items-center gap-2"><Clock size={16} />Planification de l'import</CardTitle>
|
<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>
|
</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">
|
<div className="space-y-2 max-w-xs">
|
||||||
<Label>Heure d'import quotidien</Label>
|
<Label>Heure d'import quotidien</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -474,6 +504,41 @@ export default function SettingsPage() {
|
|||||||
Prochain import : demain à {form.import_time || "06:00"}
|
Prochain import : demain à {form.import_time || "06:00"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
@@ -33,11 +33,29 @@ async function findAvailablePort(startPort: number = 3000): Promise<number> {
|
|||||||
// ─── Tâche d'import quotidien + lecture RSS ──────────────────────────────────
|
// ─── Tâche d'import quotidien + lecture RSS ──────────────────────────────────
|
||||||
let cronJob: ReturnType<typeof cron.schedule> | null = null;
|
let cronJob: ReturnType<typeof cron.schedule> | null = null;
|
||||||
|
|
||||||
async function scheduleDailyImport() {
|
export async function scheduleDailyImport() {
|
||||||
// Heure configurable, défaut 06:00
|
|
||||||
const importTime = (await getSetting("import_time")) || "06:00";
|
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 [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) {
|
if (cronJob) {
|
||||||
cronJob.stop();
|
cronJob.stop();
|
||||||
cronJob = null;
|
cronJob = null;
|
||||||
@@ -63,7 +81,6 @@ async function scheduleDailyImport() {
|
|||||||
console.error("[Cron] Erreur lors de la lecture RSS:", e);
|
console.error("[Cron] Erreur lors de la lecture RSS:", e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
console.log(`[Cron] Import quotidien + lecture RSS planifiés à ${importTime} (${cronExpr})`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import {
|
|||||||
saveRssSettings,
|
saveRssSettings,
|
||||||
} from "./db";
|
} from "./db";
|
||||||
import { importVeille, importAAP, runFullImport, getImportConfig } from "./importer";
|
import { importVeille, importAAP, runFullImport, getImportConfig } from "./importer";
|
||||||
import { scheduleRssFetch } from "./_core/index";
|
import { scheduleDailyImport } from "./_core/index";
|
||||||
import { loginLocalUser, hashPassword, ensureAdminExists } from "./localAuth";
|
import { loginLocalUser, hashPassword, ensureAdminExists } from "./localAuth";
|
||||||
import { classifyArticle } from "./aiClassifier";
|
import { classifyArticle } from "./aiClassifier";
|
||||||
import { getDb } from "./db";
|
import { getDb } from "./db";
|
||||||
@@ -372,6 +372,8 @@ export const appRouter = router({
|
|||||||
sharepoint_token: z.string().optional(),
|
sharepoint_token: z.string().optional(),
|
||||||
auth_mode: z.enum(["local", "free"]).optional(),
|
auth_mode: z.enum(["local", "free"]).optional(),
|
||||||
import_time: z.string().optional(),
|
import_time: z.string().optional(),
|
||||||
|
fetch_mode: z.enum(["scheduled", "interval"]).optional(),
|
||||||
|
fetch_interval_minutes: z.string().optional(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
@@ -380,6 +382,10 @@ export const appRouter = router({
|
|||||||
if (v !== undefined && v !== "••••••••") toSave[k] = v;
|
if (v !== undefined && v !== "••••••••") toSave[k] = v;
|
||||||
}
|
}
|
||||||
await setSettings(toSave);
|
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 };
|
return { success: true };
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
@@ -592,8 +598,8 @@ export const appRouter = router({
|
|||||||
}))
|
}))
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
await saveRssSettings(input);
|
await saveRssSettings(input);
|
||||||
// Recharger le planificateur RSS avec les nouveaux paramètres
|
// Recharger le planificateur (la lecture RSS est intégrée dans scheduleDailyImport)
|
||||||
await scheduleRssFetch();
|
await scheduleDailyImport();
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|||||||
7
todo.md
7
todo.md
@@ -175,3 +175,10 @@
|
|||||||
- [x] Couleur de sélection au clic sur une ligne (bg-primary/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] Filtre Lu/Non lu avec bouton segmenté (Non lu par défaut)
|
||||||
- [x] Message vide adapté selon le filtre actif
|
- [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
|
||||||
|
|||||||
Reference in New Issue
Block a user