feat: geçmiş veri motoru + gerçek AI + KİK kararları + TED + kırmızı bayrak + planlar
Roadmap'in 6 maddesi de gerçek çalışır halde:
1) Geçmiş veri motoru (EKAP v2)
- server/ekap-v2.mjs: AES-256-CBC imzalı EKAP v2 istemcisi (zero-dep),
ihale arama + sonuç ilanı parse (kazanan firma, sözleşme bedeli,
yaklaşık maliyet, teklif sayısı), TR firma adı normalize
- server/history-store.mjs: SQLite deposu (IKN+kısım anahtarlı, upsert)
- server/ekap-scan.mjs: tarih aralıklı artımlı tarayıcı (6 paralel)
- server/scheduler.mjs: sunucu içi periyodik tarama (SCAN_* env ile)
- /api/tenders artık geçmiş sonuçları da birleştiriyor → kazanan firma,
rakip ve kırmızı bayrak analizleri gerçek veriyle doluyor
2) Gerçek AI şartname analizi
- server/ai.mjs: 6 sağlayıcılı fallback zinciri (GLM/DeepSeek/Gemini/
Grok/GPT/Claude), JSON şemalı çıktı, uydurma yasak promptu
- pdfjs-dist ile tarayıcıda PDF metin çıkarma (dinamik import)
- AiSpecAnalyzerModal: sahte sonuç kaldırıldı, gerçek dosya + dürüst hata
3) KİK kurul kararları
- server/kik.mjs: EKAP v2 JSON API (Sec-1..6 imzalı), uyuşmazlık/
düzenleyici/mahkeme; karar tam metni (ayrı AES anahtarıyla URL)
- KikDecisions sekmesi: arama + takipteki idarelerden hızlı öneri
4) Fiyatlandırma + ödeme
- server/billing.mjs: Stripe Checkout (zero-dep, form-encoded)
- Pricing sekmesi: 3 plan, aylık/yıllık, yapılandırılmadıysa dürüst uyarı
5) TED (AB ihaleleri)
- server/ted.mjs: anonim TED v3 API, 3 mod (açık/tüm TR/AB'de kazanan TR)
- rate-limit korumalı (429 HTML gövdesi ele alınır), çok dilli alan çözümü
6) Kırmızı bayrak & şeffaflık
- analytics.js: redFlags (tek teklif, düşük kırım, tahmin üstü bedel,
aşırı düşük teklif, tedarikçi yoğunlaşması) + marketConcentration (HHI)
- RedFlags sekmesi: seviyeli uyarılar + yöntem şeffaflığı notu
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* 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 path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
let calisiyor = false;
|
||||
|
||||
function tara() {
|
||||
if (calisiyor) {
|
||||
console.log('[scheduler] önceki tarama sürüyor, bu tur atlandı');
|
||||
return;
|
||||
}
|
||||
calisiyor = true;
|
||||
const gun = process.env.SCAN_GUN || '3';
|
||||
const limit = process.env.SCAN_LIMIT || '300';
|
||||
console.log(`[scheduler] EKAP sonuç taraması başlıyor (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})`);
|
||||
});
|
||||
p.on('error', (e) => {
|
||||
calisiyor = false;
|
||||
console.warn('[scheduler] tarama başlatılamadı:', e.message);
|
||||
});
|
||||
}
|
||||
|
||||
export function startScheduler() {
|
||||
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 2 dk sonra ilk tur (sunucu ısınsın), sonra periyodik
|
||||
setTimeout(tara, 2 * 60 * 1000).unref?.();
|
||||
setInterval(tara, ms).unref?.();
|
||||
}
|
||||
Reference in New Issue
Block a user