fix: havuz uyarısı tek ölçüm yerine 7 günlük ortalamaya kıyaslasın (yavaş erime de yakalansın)
This commit is contained in:
+45
-10
@@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+11
-2
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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 && (
|
||||
<div
|
||||
className="flex items-center gap-1.5 text-amber-300"
|
||||
title={`ilan.gov.tr'de şu an ${havuz.son?.kaynak_toplam?.toLocaleString('tr-TR')} açık ihale var; bu hafta en yüksek ${havuz.haftalikZirve?.toLocaleString('tr-TR')} idi. Havuzun tamamı yüklendi — eksik veri yok.`}
|
||||
title={`ilan.gov.tr'de şu an ${havuz.son?.kaynak_toplam?.toLocaleString('tr-TR')} açık ihale var; son 7 günün ortalaması ${havuz.haftalikOrtalama?.toLocaleString('tr-TR')} (${havuz.olcumAdedi} ölçüm). Havuzun tamamı yüklendi — eksik veri yok.`}
|
||||
>
|
||||
<TrendingDown className="w-3.5 h-3.5" />
|
||||
<span className="font-bold">
|
||||
Kaynakta ihale sayısı düşük (%{Math.round(havuz.zirveyeOran * 100)})
|
||||
Kaynakta ihale sayısı düşük (7 gün ort. %{Math.round(havuz.ortalamayaOran * 100)})
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user