/** * Sunucu içi zamanlayıcı — geçmiş sonuç ilanlarını periyodik olarak biriktirir. * Container'da ayrı cron kurmaya gerek kalmaz; server/index.js başlatır. * * Env: * SCAN_ENABLED=0 → kapat (varsayılan açık) * SCAN_INTERVAL_SAAT=6 → tarama sıklığı (saat) * SCAN_GUN=3 → her turda geriye kaç günü tara * SCAN_LIMIT=300 → tur başına maksimum ihale */ import { spawn } from 'node:child_process'; import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { historyStats } from './history-store.mjs'; import { bildirimleriIsle } from './notifier.mjs'; import { zeyilnameTara, sonucTara } from './zeyilname.mjs'; import { briefZamanlayici } from './brief.mjs'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); let calisiyor = false; // Arşiv boş/küçükse geniş geçmiş dolumu, doluysa artımlı günlük tarama. function tarataParams() { const s = historyStats(); const mevcut = s.available ? (s.toplam || 0) : 0; const esik = Number(process.env.SCAN_BACKFILL_ESIK || 25000); if (mevcut < esik) { return { gun: process.env.SCAN_BACKFILL_GUN || '365', limit: process.env.SCAN_BACKFILL_LIMIT || '1500', tip: `geçmiş dolumu (arşivde ${mevcut} kayıt)`, }; } return { gun: process.env.SCAN_GUN || '3', limit: process.env.SCAN_LIMIT || '300', tip: `artımlı güncelleme (arşivde ${mevcut} kayıt)`, }; } function tara() { if (calisiyor) { console.log('[scheduler] önceki tarama sürüyor, bu tur atlandı'); return; } calisiyor = true; const { gun, limit, tip } = tarataParams(); console.log(`[scheduler] EKAP sonuç taraması — ${tip}: son ${gun} gün, limit ${limit}`); const p = spawn(process.execPath, [path.join(__dirname, 'ekap-scan.mjs'), '--gun', gun, '--limit', limit], { stdio: ['ignore', 'pipe', 'pipe'], }); p.stdout.on('data', (d) => process.stdout.write(String(d))); p.stderr.on('data', (d) => process.stderr.write(String(d))); p.on('close', (code) => { calisiyor = false; console.log(`[scheduler] tarama bitti (çıkış kodu ${code})`); // Taramanın ardından eksik OKAS kalemlerini doldur (fiyat analizi için) if (code === 0 && process.env.KALEM_DOLDUR !== '0') kalemDoldur(); }); p.on('error', (e) => { calisiyor = false; console.warn('[scheduler] tarama başlatılamadı:', e.message); }); } // Eksik OKAS kalemlerini arka planda doldurur (kategori bazlı fiyat analizi) function kalemDoldur() { const n = process.env.KALEM_DOLDUR_LIMIT || '150'; const p = spawn(process.execPath, [path.join(__dirname, 'ekap-scan.mjs'), '--kalem-doldur', n], { stdio: ['ignore', 'pipe', 'pipe'], }); p.stdout.on('data', (d) => process.stdout.write(String(d))); p.on('error', (e) => console.warn('[scheduler] kalem doldurma başlatılamadı:', e.message)); } // --- Doğrudan temin (Playwright/Chromium gerektirir) --- let dtCalisiyor = false; let dtSonTur = null; // { baslangic, bitis, kod, hata } let dtSonrakiMs = null; // Chromium ~350MB RSS kullanabilir; sunucu darsa DT'yi atla ve uyar. function bellekYeterliMi() { const bosMB = Math.round(os.freemem() / 1024 / 1024); const esik = Number(process.env.DT_MIN_BOS_MB || 600); if (bosMB < esik) { console.warn(`[scheduler] ⚠ Sunucu belleği dar (${bosMB}MB boş < ${esik}MB) — doğrudan temin taraması bu tur atlandı. Kalıcı sorun olursa DT_ENABLED=0 ile kapatın.`); return false; } return true; } function dtTara() { if (process.env.DT_ENABLED === '0') return; if (dtCalisiyor) { console.log('[scheduler] önceki DT taraması sürüyor, bu tur atlandı'); return; } if (!bellekYeterliMi()) return; dtCalisiyor = true; dtSonTur = { baslangic: new Date().toISOString() }; const pages = process.env.DT_PAGES || '5'; // 30 detay/4 saat ile 10 bin kayıt 55 günde dolar; sektör sınıflandırması // detaya bağlı olduğu için bütçe yükseltildi. const details = process.env.DT_DETAILS || '250'; console.log(`[scheduler] doğrudan temin taraması başlıyor (${pages} sayfa, ${details} detay)`); const p = spawn(process.execPath, [path.join(__dirname, 'dt-worker.mjs'), '--pages', pages, '--details', details], { stdio: ['ignore', 'pipe', 'pipe'], }); p.stdout.on('data', (d) => process.stdout.write(String(d))); p.stderr.on('data', (d) => process.stderr.write(String(d))); p.on('close', (code) => { dtCalisiyor = false; dtSonTur = { ...(dtSonTur || {}), bitis: new Date().toISOString(), kod: code }; if (code !== 0) console.warn(`[scheduler] ⚠ DT taraması ${code} koduyla bitti — Chromium kurulu mu / bellek yeterli mi kontrol edin.`); else console.log('[scheduler] DT taraması bitti'); }); p.on('error', (e) => { dtCalisiyor = false; dtSonTur = { ...(dtSonTur || {}), bitis: new Date().toISOString(), hata: e.message }; console.warn('[scheduler] ⚠ DT worker başlatılamadı:', e.message, '— Chromium yoksa DT_ENABLED=0 yapın.'); }); } export function schedulerStatus() { const bosMB = Math.round(os.freemem() / 1024 / 1024); return { sonucTaramasi: { aktif: process.env.SCAN_ENABLED !== '0', calisiyor: calisiyor, aralikSaat: Number(process.env.SCAN_INTERVAL_SAAT || 6) }, dogrudanTemin: { aktif: process.env.DT_ENABLED !== '0', calisiyor: dtCalisiyor, aralikSaat: Number(process.env.DT_INTERVAL_SAAT || 4), sonTur: dtSonTur, bellekEsigiMB: Number(process.env.DT_MIN_BOS_MB || 600), }, bosBellekMB: bosMB, toplamBellekMB: Math.round(os.totalmem() / 1024 / 1024), }; } // --- Otomatik e-posta bildirimi --- let bildirimCalisiyor = false; async function bildirimTuru(tenderGetir) { if (bildirimCalisiyor) return; bildirimCalisiyor = true; try { const s = await bildirimleriIsle(tenderGetir); if (s.atlandi) console.log(`[scheduler] bildirim atlandı: ${s.atlandi}`); else console.log(`[scheduler] bildirim turu: ${s.abonelik} abonelik, ${s.gonderilen || 0} e-posta, ${s.hata || 0} hata`); } catch (e) { console.warn('[scheduler] bildirim hatası:', e.message); } finally { bildirimCalisiyor = false; } } export function startScheduler({ tenderGetir } = {}) { if (process.env.SCAN_ENABLED === '0') { console.log('[scheduler] devre dışı (SCAN_ENABLED=0)'); return; } const saat = Number(process.env.SCAN_INTERVAL_SAAT || 6); const ms = Math.max(1, saat) * 3600 * 1000; console.log(`[scheduler] geçmiş sonuç taraması her ${saat} saatte bir çalışacak`); // Açılıştan 90 sn sonra ilk tur (sunucu ısınsın), sonra periyodik setTimeout(tara, 90 * 1000).unref?.(); setInterval(tara, ms).unref?.(); // Doğrudan temin: sonuç taramasıyla çakışmasın diye kaydırmalı if (process.env.DT_ENABLED === '0') { console.log('[scheduler] doğrudan temin devre dışı (DT_ENABLED=0)'); } else { const dtSaat = Number(process.env.DT_INTERVAL_SAAT || 4); console.log(`[scheduler] doğrudan temin taraması her ${dtSaat} saatte bir çalışacak`); setTimeout(dtTara, 4 * 60 * 1000).unref?.(); setInterval(dtTara, Math.max(1, dtSaat) * 3600 * 1000).unref?.(); } // Takipteki ihaleler: zeyilname/düzeltme ilanı + sonuç bildirimi if (process.env.TAKIP_ENABLED === '0') { console.log('[scheduler] takip izleme devre dışı (TAKIP_ENABLED=0)'); } else { const tSaat = Number(process.env.TAKIP_INTERVAL_SAAT || 3); const takipTuru = () => Promise.all([zeyilnameTara(), sonucTara()]).catch((e) => console.warn('[scheduler] takip:', e.message)); console.log(`[scheduler] takip izleme (zeyilname/sonuç) her ${tSaat} saatte bir`); setTimeout(takipTuru, 8 * 60 * 1000).unref?.(); setInterval(takipTuru, Math.max(1, tSaat) * 3600 * 1000).unref?.(); } // Haftalık brief (Pazartesi 08:00): takipli ihalelerin tarihleri + sonuçlar if (tenderGetir) briefZamanlayici(tenderGetir); // Otomatik e-posta bildirimi if (process.env.NOTIFY_ENABLED === '0' || !tenderGetir) { console.log('[scheduler] otomatik e-posta bildirimi devre dışı'); } else { const nSaat = Number(process.env.NOTIFY_INTERVAL_SAAT || 6); console.log(`[scheduler] otomatik e-posta bildirimi her ${nSaat} saatte bir kontrol edilecek`); setTimeout(() => bildirimTuru(tenderGetir), 6 * 60 * 1000).unref?.(); setInterval(() => bildirimTuru(tenderGetir), Math.max(1, nSaat) * 3600 * 1000).unref?.(); } }