Checkpoint: Suppression du cron RSS indépendant. La lecture des flux RSS est désormais intégrée dans le cron d'import quotidien (import_time dans Paramètres). Le panneau de planification RSS dans RssFeeds affiche maintenant l'heure configurée et redirige vers Paramètres pour la modifier. 0 erreur TypeScript.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"version": "d5ca5837",
|
||||
"timestamp": 1782484339948
|
||||
"version": "138ac2fc",
|
||||
"timestamp": 1782484729381
|
||||
}
|
||||
@@ -539,127 +539,31 @@ function FeedCard({
|
||||
|
||||
// ─── Panneau de configuration ─────────────────────────────────────────────────
|
||||
|
||||
function SettingsPanel({ isAdmin }: { isAdmin: boolean }) {
|
||||
const { data: settings, refetch } = trpc.rss.getSettings.useQuery();
|
||||
const saveMutation = trpc.rss.saveSettings.useMutation({
|
||||
onSuccess: () => {
|
||||
toast.success("Paramètres RSS sauvegardés");
|
||||
refetch();
|
||||
},
|
||||
onError: (e) => toast.error("Erreur", { description: e.message }),
|
||||
});
|
||||
|
||||
const [fetchMode, setFetchMode] = useState<"interval" | "scheduled">(settings?.fetchMode ?? "scheduled");
|
||||
const [fetchIntervalMinutes, setFetchIntervalMinutes] = useState(settings?.fetchIntervalMinutes ?? 360);
|
||||
const [scheduledTime, setScheduledTime] = useState(settings?.scheduledTime ?? "06:00");
|
||||
const [autoFetchEnabled, setAutoFetchEnabled] = useState(settings?.autoFetchEnabled ?? true);
|
||||
|
||||
// Sync state when settings load
|
||||
if (settings && settings.fetchMode !== fetchMode && !saveMutation.isPending) {
|
||||
setFetchMode(settings.fetchMode);
|
||||
setFetchIntervalMinutes(settings.fetchIntervalMinutes);
|
||||
setScheduledTime(settings.scheduledTime ?? "06:00");
|
||||
setAutoFetchEnabled(settings.autoFetchEnabled);
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
saveMutation.mutate({
|
||||
fetchMode,
|
||||
fetchIntervalMinutes,
|
||||
scheduledTime,
|
||||
autoFetchEnabled,
|
||||
});
|
||||
};
|
||||
function SettingsPanel({ isAdmin: _isAdmin }: { isAdmin: boolean }) {
|
||||
const { data: settings } = trpc.rss.getSettings.useQuery();
|
||||
const importTime = settings?.scheduledTime ?? "06:00";
|
||||
|
||||
return (
|
||||
<Card className="border-2 border-dashed border-primary/20">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<Settings2 className="h-4 w-4 text-primary" />
|
||||
Paramètres de lecture automatique
|
||||
Planification de la lecture RSS
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Configurez la fréquence et le mode de lecture des flux RSS.
|
||||
La lecture des flux RSS est déclenchée automatiquement par le cron d’import quotidien.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-5">
|
||||
{/* Activation */}
|
||||
<div className="flex items-center justify-between p-3 bg-muted/50 rounded-lg">
|
||||
<CardContent>
|
||||
<div className="flex items-center gap-3 p-4 rounded-lg bg-muted/50 border border-border">
|
||||
<Clock className="h-5 w-5 text-primary flex-shrink-0" />
|
||||
<div>
|
||||
<p className="font-medium text-sm">Lecture automatique</p>
|
||||
<p className="text-xs text-muted-foreground">Active la récupération périodique des flux</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={autoFetchEnabled}
|
||||
onCheckedChange={setAutoFetchEnabled}
|
||||
disabled={!isAdmin}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Mode */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium">Mode de planification</Label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{(["scheduled", "interval"] as const).map((mode) => (
|
||||
<button
|
||||
key={mode}
|
||||
type="button"
|
||||
disabled={!isAdmin}
|
||||
onClick={() => setFetchMode(mode)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 p-3 rounded-lg border-2 text-left transition-all text-sm",
|
||||
fetchMode === mode ? "border-primary bg-primary/5" : "border-border hover:border-primary/40",
|
||||
!isAdmin && "opacity-50 cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
<Clock className={cn("h-4 w-4", fetchMode === 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 minutes"}</p>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
<p className="font-medium text-sm">Lecture automatique chaque jour à <span className="text-primary font-bold">{importTime}</span></p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Pour modifier l’heure, rendez-vous dans <strong>Paramètres → Import & Planification</strong>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Paramètre selon le mode */}
|
||||
{fetchMode === "scheduled" ? (
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-sm">Heure de lecture quotidienne</Label>
|
||||
<Input
|
||||
type="time"
|
||||
value={scheduledTime}
|
||||
onChange={(e) => setScheduledTime(e.target.value)}
|
||||
disabled={!isAdmin}
|
||||
className="w-36"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-sm">Intervalle de lecture</Label>
|
||||
<Select
|
||||
value={String(fetchIntervalMinutes)}
|
||||
onValueChange={(v) => setFetchIntervalMinutes(Number(v))}
|
||||
disabled={!isAdmin}
|
||||
>
|
||||
<SelectTrigger className="w-48">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{INTERVAL_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={String(opt.value)}>{opt.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isAdmin && (
|
||||
<Button onClick={handleSave} disabled={saveMutation.isPending} size="sm" className="gap-2">
|
||||
<Save className="h-3.5 w-3.5" />
|
||||
{saveMutation.isPending ? "Enregistrement…" : "Sauvegarder les paramètres"}
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -12,7 +12,7 @@ import { runFullImport } from "../importer";
|
||||
import uploadRoutes from "../uploadRoutes";
|
||||
import scheduledRoutes from "../scheduledRoutes";
|
||||
import { ensureAdminExists } from "../localAuth";
|
||||
import { getSetting, getRssSettings } from "../db";
|
||||
import { getSetting } from "../db";
|
||||
import { runRssFetch } from "../rssEngine";
|
||||
|
||||
function isPortAvailable(port: number): Promise<boolean> {
|
||||
@@ -30,7 +30,7 @@ async function findAvailablePort(startPort: number = 3000): Promise<number> {
|
||||
throw new Error(`No available port found starting from ${startPort}`);
|
||||
}
|
||||
|
||||
// ─── Tâche d'import quotidien ─────────────────────────────────────────────────
|
||||
// ─── Tâche d'import quotidien + lecture RSS ──────────────────────────────────
|
||||
let cronJob: ReturnType<typeof cron.schedule> | null = null;
|
||||
|
||||
async function scheduleDailyImport() {
|
||||
@@ -45,85 +45,34 @@ async function scheduleDailyImport() {
|
||||
cronJob = cron.schedule(cronExpr, async () => {
|
||||
console.log(`[Cron] Import automatique démarré à ${new Date().toISOString()}`);
|
||||
try {
|
||||
// 1. Import des fichiers Excel (Veille + AAP)
|
||||
const result = await runFullImport();
|
||||
console.log(`[Cron] Import terminé — Veille: +${result.veille.newRows} | AAP: +${result.aap.newRows}`);
|
||||
console.log(`[Cron] Import Excel terminé — Veille: +${result.veille.newRows} | AAP: +${result.aap.newRows}`);
|
||||
} catch (e) {
|
||||
console.error("[Cron] Erreur lors de l'import:", e);
|
||||
console.error("[Cron] Erreur lors de l'import Excel:", e);
|
||||
}
|
||||
});
|
||||
console.log(`[Cron] Import quotidien planifié à ${importTime} (${cronExpr})`);
|
||||
}
|
||||
|
||||
// ─── Planificateur RSS natif ──────────────────────────────────────────────────
|
||||
let rssCronJob: ReturnType<typeof cron.schedule> | null = null;
|
||||
|
||||
/**
|
||||
* Démarre (ou redémarre) le planificateur RSS en lisant la configuration
|
||||
* depuis la table rss_settings. Peut être appelé au démarrage et à chaque
|
||||
* modification des paramètres RSS via l'interface d'administration.
|
||||
*/
|
||||
export async function scheduleRssFetch() {
|
||||
// Arrêter le cron existant s'il y en a un
|
||||
if (rssCronJob) {
|
||||
rssCronJob.stop();
|
||||
rssCronJob = null;
|
||||
console.log("[RSS Cron] Planificateur RSS arrêté.");
|
||||
}
|
||||
|
||||
let settings = null;
|
||||
try {
|
||||
settings = await getRssSettings();
|
||||
} catch (e) {
|
||||
// En cas d'erreur (ex: colonne manquante lors d'une migration),
|
||||
// utiliser les valeurs par défaut et planifier un retry dans 2 minutes
|
||||
console.warn("[RSS Cron] Impossible de lire les paramètres RSS, utilisation des valeurs par défaut:", (e as Error).message);
|
||||
setTimeout(() => scheduleRssFetch(), 2 * 60 * 1000);
|
||||
// Démarrer quand même avec les valeurs par défaut
|
||||
settings = { autoFetchEnabled: true, fetchMode: "interval" as const, fetchIntervalMinutes: 60, scheduledTime: "06:00" };
|
||||
}
|
||||
|
||||
if (!settings || !settings.autoFetchEnabled) {
|
||||
console.log("[RSS Cron] Lecture automatique des flux RSS désactivée.");
|
||||
return;
|
||||
}
|
||||
|
||||
let cronExpr: string;
|
||||
|
||||
if (settings.fetchMode === "interval") {
|
||||
// Mode intervalle : toutes les N minutes
|
||||
const intervalMin = Math.max(5, settings.fetchIntervalMinutes ?? 60);
|
||||
if (intervalMin < 60) {
|
||||
cronExpr = `*/${intervalMin} * * * *`;
|
||||
} else if (intervalMin % 60 === 0) {
|
||||
const hours = intervalMin / 60;
|
||||
cronExpr = `0 */${hours} * * *`;
|
||||
} else {
|
||||
cronExpr = `*/${intervalMin} * * * *`;
|
||||
}
|
||||
console.log(`[RSS Cron] Mode intervalle — toutes les ${intervalMin} minutes (${cronExpr})`);
|
||||
} else {
|
||||
// Mode planifié : heure fixe quotidienne
|
||||
const scheduledTime = settings.scheduledTime ?? "06:00";
|
||||
const [hour, minute] = scheduledTime.split(":").map(Number);
|
||||
cronExpr = `0 ${minute ?? 0} ${hour ?? 6} * * *`;
|
||||
console.log(`[RSS Cron] Mode planifié — tous les jours à ${scheduledTime} (${cronExpr})`);
|
||||
}
|
||||
|
||||
rssCronJob = cron.schedule(cronExpr, async () => {
|
||||
console.log(`[RSS Cron] Lecture des flux RSS démarrée à ${new Date().toISOString()}`);
|
||||
try {
|
||||
const summary = await runRssFetch();
|
||||
// 2. Lecture des flux RSS
|
||||
const rssSummary = await runRssFetch();
|
||||
console.log(
|
||||
`[RSS Cron] Lecture terminée — ${summary.totalFeeds} flux, ` +
|
||||
`+${summary.totalNewItems} nouveaux articles, ` +
|
||||
`${summary.errorFeeds} erreur(s)`
|
||||
`[Cron] Lecture RSS terminée — ${rssSummary.totalFeeds} flux, ` +
|
||||
`+${rssSummary.totalNewItems} nouveaux articles, ` +
|
||||
`${rssSummary.errorFeeds} erreur(s)`
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("[RSS Cron] Erreur lors de la lecture des flux:", e);
|
||||
console.error("[Cron] Erreur lors de la lecture RSS:", e);
|
||||
}
|
||||
});
|
||||
console.log(`[Cron] Import quotidien + lecture RSS planifiés à ${importTime} (${cronExpr})`);
|
||||
}
|
||||
|
||||
console.log("[RSS Cron] Planificateur RSS démarré.");
|
||||
/**
|
||||
* Stub conservé pour compatibilité avec les imports existants dans routers.ts.
|
||||
* La lecture RSS est désormais intégrée dans scheduleDailyImport().
|
||||
*/
|
||||
export async function scheduleRssFetch() {
|
||||
// No-op : la lecture RSS est pilotée par le cron d'import quotidien (import_time)
|
||||
console.log("[RSS Cron] Planificateur RSS indépendant désactivé — la lecture est pilotée par le cron d'import quotidien.");
|
||||
}
|
||||
|
||||
async function startServer() {
|
||||
@@ -159,7 +108,6 @@ async function startServer() {
|
||||
try {
|
||||
await ensureAdminExists();
|
||||
await scheduleDailyImport();
|
||||
await scheduleRssFetch();
|
||||
} catch (e) {
|
||||
console.error("[Init] Erreur d'initialisation:", e);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user