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 } 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 + lecture RSS ────────────────────────────────── let cronJob: ReturnType | null = null; export async function scheduleDailyImport() { const importTime = (await getSetting("import_time")) || "06:00"; const fetchMode = (await getSetting("fetch_mode")) || "daily"; const fetchIntervalMinutes = parseInt((await getSetting("fetch_interval_minutes")) || "1440", 10); const fetchWeekDay = (await getSetting("fetch_week_day")) || "1"; // 0=dim, 1=lun, ..., 6=sam const fetchMonthDay = (await getSetting("fetch_month_day")) || "1"; // 1-28 const [hour, minute] = importTime.split(":").map(Number); const h = hour ?? 6; const m = minute ?? 0; let cronExpr: string; let cronLabel: string; if (fetchMode === "interval") { const intervalMin = Math.max(60, fetchIntervalMinutes); if (intervalMin % 60 === 0) { const hours = intervalMin / 60; cronExpr = hours === 24 ? `0 ${m} ${h} * * *` : `0 ${m} */${hours} * * *`; } else { cronExpr = `0 */${intervalMin} * * * *`; } const hours = intervalMin / 60; cronLabel = `Mode intervalle — toutes les ${hours >= 1 ? hours + "h" : intervalMin + "min"} (${cronExpr})`; } else if (fetchMode === "weekly") { // hebdomadaire : le jour choisi à l'heure choisie cronExpr = `0 ${m} ${h} * * ${fetchWeekDay}`; const jours = ["dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"]; cronLabel = `Mode hebdomadaire — chaque ${jours[parseInt(fetchWeekDay)] ?? "lundi"} à ${importTime} (${cronExpr})`; } else if (fetchMode === "monthly") { // mensuel : le jour du mois choisi à l'heure choisie const day = Math.min(28, Math.max(1, parseInt(fetchMonthDay))); cronExpr = `0 ${m} ${h} ${day} * *`; cronLabel = `Mode mensuel — le ${day} de chaque mois à ${importTime} (${cronExpr})`; } else { // daily (défaut) cronExpr = `0 ${m} ${h} * * *`; cronLabel = `Mode quotidien — tous les jours à ${importTime} (${cronExpr})`; } console.log(`[Cron] ${cronLabel}`); if (cronJob) { cronJob.stop(); cronJob = null; } 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 Excel terminé — Veille: +${result.veille.newRows} | AAP: +${result.aap.newRows}`); } catch (e) { console.error("[Cron] Erreur lors de l'import Excel:", e); } try { // 2. Lecture des flux RSS const rssSummary = await runRssFetch(); console.log( `[Cron] Lecture RSS terminée — ${rssSummary.totalFeeds} flux, ` + `+${rssSummary.totalNewItems} nouveaux articles, ` + `${rssSummary.errorFeeds} erreur(s)` ); } catch (e) { console.error("[Cron] Erreur lors de la lecture RSS:", e); } }); } /** * 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() { 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(); } catch (e) { console.error("[Init] Erreur d'initialisation:", e); } }); } startServer().catch(console.error);