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:
@@ -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')
|
.prepare('SELECT zaman, kaynak_toplam, yuklenen FROM havuz_olcum ORDER BY id DESC LIMIT 1')
|
||||||
.get() || null;
|
.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(
|
db.prepare(
|
||||||
`INSERT INTO havuz_olcum (zaman, kaynak_toplam, yuklenen, eksik, dt, arsiv)
|
`INSERT INTO havuz_olcum (zaman, kaynak_toplam, yuklenen, eksik, dt, arsiv)
|
||||||
VALUES (?,?,?,?,?,?)`
|
VALUES (?,?,?,?,?,?)`
|
||||||
@@ -266,25 +274,52 @@ export function havuzOlc({ kaynakToplam, yuklenen, dt = null, arsiv = null }) {
|
|||||||
const sinir = new Date(Date.now() - 90 * 864e5).toISOString();
|
const sinir = new Date(Date.now() - 90 * 864e5).toISOString();
|
||||||
db.prepare('DELETE FROM havuz_olcum WHERE zaman < ?').run(sinir);
|
db.prepare('DELETE FROM havuz_olcum WHERE zaman < ?').run(sinir);
|
||||||
|
|
||||||
return degerlendir(onceki, { kaynakToplam, yuklenen });
|
return degerlendir(onceki, ref, { kaynakToplam, yuklenen });
|
||||||
} finally {
|
} finally {
|
||||||
db.close();
|
db.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Ölçümü önceki ölçümle karşılaştırır; uyarı gerekiyorsa açıklamasını döndürür. */
|
// Ortalamanın anlamlı olması için gereken en az ölçüm sayısı.
|
||||||
function degerlendir(onceki, { kaynakToplam, yuklenen }) {
|
const MIN_OLCUM = 5;
|
||||||
const sonuc = { uyari: null, onceki: onceki?.kaynak_toplam ?? null, simdi: kaynakToplam ?? null };
|
// 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)
|
// 1) Kaynak düştü mü? (bizim hatamız değil ama bilmek isteriz)
|
||||||
if (onceki?.kaynak_toplam > 0 && kaynakToplam != null) {
|
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;
|
const oran = kaynakToplam / onceki.kaynak_toplam;
|
||||||
if (oran < 0.5) {
|
if (oran < 0.5) {
|
||||||
sonuc.uyari = `Kaynak havuzu %${Math.round((1 - oran) * 100)} düştü: ` +
|
sonuc.uyari = `Kaynak havuzu %${Math.round((1 - oran) * 100)} düştü: ` +
|
||||||
`${onceki.kaynak_toplam} → ${kaynakToplam} ilan (ilan.gov.tr tarafı).`;
|
`${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';
|
sonuc.tur = 'kaynak_dusus';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 2) Biz kaynağın gerisinde mi kaldık? (bu BİZİM hatamız olur)
|
// 2) Biz kaynağın gerisinde mi kaldık? (bu BİZİM hatamız olur)
|
||||||
if (!sonuc.uyari && kaynakToplam > 0 && yuklenen != null && yuklenen < kaynakToplam * 0.9) {
|
if (!sonuc.uyari && kaynakToplam > 0 && yuklenen != null && yuklenen < kaynakToplam * 0.9) {
|
||||||
|
|||||||
+11
-2
@@ -970,13 +970,22 @@ const server = http.createServer(async (req, res) => {
|
|||||||
const son = gecmis[0] || null;
|
const son = gecmis[0] || null;
|
||||||
// Son 7 günün en yükseği — bugünkü sayı buna göre kıyaslanır.
|
// 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 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);
|
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, {
|
return json(res, 200, {
|
||||||
success: true,
|
success: true,
|
||||||
son,
|
son,
|
||||||
|
haftalikOrtalama: ortalama,
|
||||||
haftalikZirve: zirve || null,
|
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,
|
olcumSayisi: gecmis.length,
|
||||||
gecmis,
|
gecmis,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ export const LiveStatsBar = () => {
|
|||||||
const takibimdeki = tenders.filter((t) => t.takipte).length;
|
const takibimdeki = tenders.filter((t) => t.takipte).length;
|
||||||
const kalemSayisi = tenders.reduce((s, t) => s + (t.items?.length || 0), 0);
|
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.
|
// 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);
|
const [havuz, setHavuz] = useState(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/api/health/havuz?limit=40')
|
fetch('/api/health/havuz?limit=40')
|
||||||
@@ -18,7 +20,7 @@ export const LiveStatsBar = () => {
|
|||||||
.then((d) => setHavuz(d.success ? d : null))
|
.then((d) => setHavuz(d.success ? d : null))
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
const dusuk = havuz?.zirveyeOran != null && havuz.zirveyeOran < 0.6;
|
const dusuk = havuz?.yeterliVeri && havuz.ortalamayaOran != null && havuz.ortalamayaOran < 0.6;
|
||||||
|
|
||||||
const stats = [
|
const stats = [
|
||||||
{ deger: liveStats.toplamIhale, etiket: 'Canlı İhale Havuzu', renk: 'text-white', dot: true },
|
{ deger: liveStats.toplamIhale, etiket: 'Canlı İhale Havuzu', renk: 'text-white', dot: true },
|
||||||
@@ -50,11 +52,11 @@ export const LiveStatsBar = () => {
|
|||||||
{dusuk && (
|
{dusuk && (
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-1.5 text-amber-300"
|
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" />
|
<TrendingDown className="w-3.5 h-3.5" />
|
||||||
<span className="font-bold">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user