import "dotenv/config"; import express from "express"; import { createServer } from "http"; import net from "net"; import { createExpressMiddleware } from "@trpc/server/adapters/express"; import * as cron from "node-cron"; import { registerOAuthRoutes } from "./oauth"; import { appRouter } from "../routers"; import { createContext } from "./context"; import { serveStatic, setupVite } from "./vite"; import { runFullImport } from "../importer"; import uploadRoutes from "../uploadRoutes"; import scheduledRoutes from "../scheduledRoutes"; import { ensureAdminExists } from "../localAuth"; import { getSetting, getRssSettings } from "../db"; import { runRssFetch } from "../rssEngine"; function isPortAvailable(port: number): Promise { return new Promise(resolve => { const server = net.createServer(); server.listen(port, () => { server.close(() => resolve(true)); }); server.on("error", () => resolve(false)); }); } async function findAvailablePort(startPort: number = 3000): Promise { for (let port = startPort; port < startPort + 20; port++) { if (await isPortAvailable(port)) return port; } throw new Error(`No available port found starting from ${startPort}`); } // ─── Tâche d'import quotidien ───────────────────────────────────────────────── let cronJob: ReturnType | null = null; async function scheduleDailyImport() { // Heure configurable, défaut 06:00 const importTime = (await getSetting("import_time")) || "06:00"; const [hour, minute] = importTime.split(":").map(Number); const cronExpr = `0 ${minute ?? 0} ${hour ?? 6} * * *`; if (cronJob) { cronJob.stop(); cronJob = null; } cronJob = cron.schedule(cronExpr, async () => { console.log(`[Cron] Import automatique démarré à ${new Date().toISOString()}`); try { const result = await runFullImport(); console.log(`[Cron] Import terminé — Veille: +${result.veille.newRows} | AAP: +${result.aap.newRows}`); } catch (e) { console.error("[Cron] Erreur lors de l'import:", e); } }); console.log(`[Cron] Import quotidien planifié à ${importTime} (${cronExpr})`); } // ─── Planificateur RSS natif ────────────────────────────────────────────────── let rssCronJob: ReturnType | 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(); console.log( `[RSS Cron] Lecture terminée — ${summary.totalFeeds} flux, ` + `+${summary.totalNewItems} nouveaux articles, ` + `${summary.errorFeeds} erreur(s)` ); } catch (e) { console.error("[RSS Cron] Erreur lors de la lecture des flux:", e); } }); console.log("[RSS Cron] Planificateur RSS démarré."); } async function startServer() { const app = express(); const server = createServer(app); app.use(express.json({ limit: "50mb" })); app.use(express.urlencoded({ limit: "50mb", extended: true })); registerOAuthRoutes(app); app.use(uploadRoutes); app.use(scheduledRoutes); app.use( "/api/trpc", createExpressMiddleware({ router: appRouter, createContext }) ); if (process.env.NODE_ENV === "development") { await setupVite(app, server); } else { serveStatic(app); } const preferredPort = parseInt(process.env.PORT || "3000"); const port = await findAvailablePort(preferredPort); if (port !== preferredPort) { console.log(`Port ${preferredPort} is busy, using port ${port} instead`); } server.listen(port, async () => { console.log(`Server running on http://localhost:${port}/`); // Initialisation post-démarrage try { await ensureAdminExists(); await scheduleDailyImport(); await scheduleRssFetch(); } catch (e) { console.error("[Init] Erreur d'initialisation:", e); } }); } startServer().catch(console.error);