28 depo taramasindan alinan alti ozellik. Kod kopyalanmadi (depolar lisanssiz), fikirler uygulandi. 1) Zeyilname / duzeltme ilani takibi (server/zeyilname.mjs): takipteki IKN'ler sunucuya yazilir; 3 saatte bir EKAP detayindan ilan sayisi ve ihale tarihi karsilastirilir, degisince "ne degisti" bildirimi (uygulama ici + e-posta + Telegram). Test: anlik kaydi eskitilince tur "yeni ilan yayimlandi; ihale tarihi 01.01.2026 → 08.10.2026" bildirimi uretti. 2) Sonuc bildirimi: takipteki ihale sonuclaninca "X firmasi Y'ye aldi, %Z kirim". Yalniz takiptekiler; kismi ihalede oran yazilmaz. 3) Ihaleden goreve (MOBIT-DURT): detayda "Hazirlik Gorevi Ac" → teslim tarihi ihale saati, pano "Teklif Hazirlaniyor", derin analizden gelen belge kontrol listesi gorevde; Kanban kartinda geri sayim rozeti. 4) Sirket belgesi sure takibi: Belge Kasasi'nda son kullanma tarihi girisi, dolmus / 30 gun icinde dolacak uyarisi; girise gunde bir bildirim. PATCH /api/me/documents/meta. 5) Telegram (server/telegram.mjs): TELEGRAM_BOT_TOKEN sunucuda; kullanici 6 haneli kodu bota yazar, "Dogrula" → getUpdates ile chat id baglanir. Arayuzde adim adim kurulum rehberi (BotFather → env → Redeploy → kod). Ihale ozeti, zeyilname ve sonuc bildirimleri Telegram'a da gider. 6) OCR (tesseract.js, tur): PDF metin katmani bossa (taranmis) ilk 8 sayfa OCR. Olcum: sentetik cetvelde guven 87, ~0,3 sn/sayfa; IKN ve kalemler dogru okundu. Kalem havuzu ve derin analiz artik taranmis PDF'ten de besleniyor. 7) Rakip Radari'nda KIK yasakli sorgusu baglantisi (firma adi panoya kopyalanir; EKAP sorguyu API'den vermiyor). Sinir deger hesaplayici KURULMADI: Kamu Ihale Genel Tebligi'ne ulasilamadi (mevzuat.gov.tr / ihale.gov.tr bu makineden erisilemiyor); formul kaynaksiz yazilmadi. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
203 lines
8.2 KiB
JavaScript
203 lines
8.2 KiB
JavaScript
/**
|
||
* 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';
|
||
|
||
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?.();
|
||
}
|
||
// 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?.();
|
||
}
|
||
}
|