diff --git a/server/history-store.mjs b/server/history-store.mjs index 9112bbb..0eb866a 100644 --- a/server/history-store.mjs +++ b/server/history-store.mjs @@ -251,6 +251,14 @@ export function havuzOlc({ kaynakToplam, yuklenen, dt = null, arsiv = null }) { .prepare('SELECT zaman, kaynak_toplam, yuklenen FROM havuz_olcum ORDER BY id DESC LIMIT 1') .get() || null; + // 7 günlük referans — tek bir ölçüme değil, ortalamaya kıyaslarız ki + // havuz yavaş yavaş erirse de yakalayalım. + const haftaBas = new Date(Date.now() - 7 * 864e5).toISOString(); + const ref = db + .prepare(`SELECT AVG(kaynak_toplam) ort, COUNT(kaynak_toplam) adet + FROM havuz_olcum WHERE zaman >= ? AND kaynak_toplam IS NOT NULL`) + .get(haftaBas) || {}; + db.prepare( `INSERT INTO havuz_olcum (zaman, kaynak_toplam, yuklenen, eksik, dt, arsiv) VALUES (?,?,?,?,?,?)` @@ -266,23 +274,50 @@ export function havuzOlc({ kaynakToplam, yuklenen, dt = null, arsiv = null }) { const sinir = new Date(Date.now() - 90 * 864e5).toISOString(); db.prepare('DELETE FROM havuz_olcum WHERE zaman < ?').run(sinir); - return degerlendir(onceki, { kaynakToplam, yuklenen }); + return degerlendir(onceki, ref, { kaynakToplam, yuklenen }); } finally { db.close(); } } -/** Ölçümü önceki ölçümle karşılaştırır; uyarı gerekiyorsa açıklamasını döndürür. */ -function degerlendir(onceki, { kaynakToplam, yuklenen }) { - const sonuc = { uyari: null, onceki: onceki?.kaynak_toplam ?? null, simdi: kaynakToplam ?? null }; +// Ortalamanın anlamlı olması için gereken en az ölçüm sayısı. +const MIN_OLCUM = 5; +// Bu oranın altına düşerse uyarılır (7 günlük ortalamanın %60'ı). +const DUSUS_ESIGI = 0.6; + +/** + * Ölçümü 7 GÜNLÜK ORTALAMAYA kıyaslar; yeterli geçmiş yoksa bir önceki + * ölçüme düşer. Uyarı gerekiyorsa açıklamasını döndürür. + */ +function degerlendir(onceki, ref, { kaynakToplam, yuklenen }) { + const ortalama = ref?.adet >= MIN_OLCUM && ref.ort ? Math.round(ref.ort) : null; + const sonuc = { + uyari: null, + onceki: onceki?.kaynak_toplam ?? null, + simdi: kaynakToplam ?? null, + haftalikOrtalama: ortalama, + olcumAdedi: ref?.adet ?? 0, + }; // 1) Kaynak düştü mü? (bizim hatamız değil ama bilmek isteriz) - if (onceki?.kaynak_toplam > 0 && kaynakToplam != null) { - const oran = kaynakToplam / onceki.kaynak_toplam; - if (oran < 0.5) { - sonuc.uyari = `Kaynak havuzu %${Math.round((1 - oran) * 100)} düştü: ` + - `${onceki.kaynak_toplam} → ${kaynakToplam} ilan (ilan.gov.tr tarafı).`; - sonuc.tur = 'kaynak_dusus'; + if (kaynakToplam != null) { + if (ortalama > 0) { + const oran = kaynakToplam / ortalama; + sonuc.ortalamayaOran = Number(oran.toFixed(3)); + if (oran < DUSUS_ESIGI) { + sonuc.uyari = `Kaynak havuzu 7 günlük ortalamanın %${Math.round((1 - oran) * 100)} altında: ` + + `ortalama ${ortalama}, şimdi ${kaynakToplam} ilan (ilan.gov.tr tarafı).`; + sonuc.tur = 'kaynak_dusus'; + } + } else if (onceki?.kaynak_toplam > 0) { + // Henüz ortalama için yeterli ölçüm yok — geçici olarak son ölçüme bakarız. + const oran = kaynakToplam / onceki.kaynak_toplam; + if (oran < 0.5) { + sonuc.uyari = `Kaynak havuzu %${Math.round((1 - oran) * 100)} düştü: ` + + `${onceki.kaynak_toplam} → ${kaynakToplam} ilan (ilan.gov.tr tarafı). ` + + `[ortalama için henüz ${ref?.adet ?? 0}/${MIN_OLCUM} ölçüm var]`; + sonuc.tur = 'kaynak_dusus'; + } } } diff --git a/server/index.js b/server/index.js index 58e4b9f..8430797 100644 --- a/server/index.js +++ b/server/index.js @@ -970,13 +970,22 @@ const server = http.createServer(async (req, res) => { const son = gecmis[0] || null; // Son 7 günün en yükseği — bugünkü sayı buna göre kıyaslanır. const haftaBas = new Date(Date.now() - 7 * 864e5).toISOString(); - const hafta = gecmis.filter((r) => r.zaman >= haftaBas); + const hafta = gecmis.filter((r) => r.zaman >= haftaBas && r.kaynak_toplam != null); const zirve = hafta.reduce((m, r) => Math.max(m, r.kaynak_toplam || 0), 0); + // Referans ölçüt ORTALAMA; zirve yalnızca bağlam için gösterilir. + const ortalama = hafta.length ? Math.round(hafta.reduce((t, r) => t + r.kaynak_toplam, 0) / hafta.length) : null; + const yeterliVeri = hafta.length >= 5; return json(res, 200, { success: true, son, + haftalikOrtalama: ortalama, haftalikZirve: zirve || null, - zirveyeOran: son?.kaynak_toplam && zirve ? Number((son.kaynak_toplam / zirve).toFixed(3)) : null, + olcumAdedi: hafta.length, + yeterliVeri, + ortalamayaOran: + yeterliVeri && son?.kaynak_toplam && ortalama + ? Number((son.kaynak_toplam / ortalama).toFixed(3)) + : null, olcumSayisi: gecmis.length, gecmis, }); diff --git a/src/components/LiveStatsBar.jsx b/src/components/LiveStatsBar.jsx index 271e6fb..f374654 100644 --- a/src/components/LiveStatsBar.jsx +++ b/src/components/LiveStatsBar.jsx @@ -9,8 +9,10 @@ export const LiveStatsBar = () => { const takibimdeki = tenders.filter((t) => t.takipte).length; const kalemSayisi = tenders.reduce((s, t) => s + (t.items?.length || 0), 0); - // Havuz sağlığı: kaynak (ilan.gov.tr) sayısı haftalık zirvenin çok altındaysa + // Havuz sağlığı: kaynak (ilan.gov.tr) sayısı 7 GÜNLÜK ORTALAMANIN çok altındaysa // bunu açıkça söyleriz — kullanıcı "veri mi kayboldu?" diye düşünmesin. + // Ortalama, tek bir ölçüme kıyaslamaktan daha güvenilir: havuz yavaş yavaş + // erirse de yakalar, tek seferlik dalgalanmada boşuna uyarı vermez. const [havuz, setHavuz] = useState(null); useEffect(() => { fetch('/api/health/havuz?limit=40') @@ -18,7 +20,7 @@ export const LiveStatsBar = () => { .then((d) => setHavuz(d.success ? d : null)) .catch(() => {}); }, []); - const dusuk = havuz?.zirveyeOran != null && havuz.zirveyeOran < 0.6; + const dusuk = havuz?.yeterliVeri && havuz.ortalamayaOran != null && havuz.ortalamayaOran < 0.6; const stats = [ { deger: liveStats.toplamIhale, etiket: 'Canlı İhale Havuzu', renk: 'text-white', dot: true }, @@ -50,11 +52,11 @@ export const LiveStatsBar = () => { {dusuk && (