feat: 30 günlük havuz referansı (kalıcı erime) + 25 sektör, çoklu sektör seçimi ve sunucudan gelen tek kaynaklı liste
This commit is contained in:
@@ -253,11 +253,17 @@ export function havuzOlc({ kaynakToplam, yuklenen, dt = null, arsiv = 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) || {};
|
||||
const ortalamaAl = (gun) => {
|
||||
const bas = new Date(Date.now() - gun * 864e5).toISOString();
|
||||
return db
|
||||
.prepare(`SELECT AVG(kaynak_toplam) ort, COUNT(kaynak_toplam) adet
|
||||
FROM havuz_olcum WHERE zaman >= ? AND kaynak_toplam IS NOT NULL`)
|
||||
.get(bas) || {};
|
||||
};
|
||||
// 7 gün: günlük dalgalanma. 30 gün: kalıcı erime — 7 günlük ortalama
|
||||
// düşüşe ayak uydurup alarmı susturduğunda bunu ancak 30 gün yakalar.
|
||||
const ref = ortalamaAl(7);
|
||||
const ref30 = ortalamaAl(30);
|
||||
|
||||
db.prepare(
|
||||
`INSERT INTO havuz_olcum (zaman, kaynak_toplam, yuklenen, eksik, dt, arsiv)
|
||||
@@ -274,7 +280,7 @@ 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, ref, { kaynakToplam, yuklenen });
|
||||
return degerlendir(onceki, ref, ref30, { kaynakToplam, yuklenen });
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
@@ -284,19 +290,26 @@ export function havuzOlc({ kaynakToplam, yuklenen, dt = null, arsiv = null }) {
|
||||
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;
|
||||
// 30 günlük referans için: daha çok ölçüm ister, eşiği daha toleranslıdır —
|
||||
// amaç ani dalgalanma değil, kalıcı erimeyi görmek.
|
||||
const MIN_OLCUM_30 = 20;
|
||||
const DUSUS_ESIGI_30 = 0.7;
|
||||
|
||||
/**
|
||||
* Ö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 }) {
|
||||
function degerlendir(onceki, ref, ref30, { kaynakToplam, yuklenen }) {
|
||||
const ortalama = ref?.adet >= MIN_OLCUM && ref.ort ? Math.round(ref.ort) : null;
|
||||
const ortalama30 = ref30?.adet >= MIN_OLCUM_30 && ref30.ort ? Math.round(ref30.ort) : null;
|
||||
const sonuc = {
|
||||
uyari: null,
|
||||
onceki: onceki?.kaynak_toplam ?? null,
|
||||
simdi: kaynakToplam ?? null,
|
||||
haftalikOrtalama: ortalama,
|
||||
aylikOrtalama: ortalama30,
|
||||
olcumAdedi: ref?.adet ?? 0,
|
||||
olcumAdedi30: ref30?.adet ?? 0,
|
||||
};
|
||||
|
||||
// 1) Kaynak düştü mü? (bizim hatamız değil ama bilmek isteriz)
|
||||
@@ -321,6 +334,21 @@ function degerlendir(onceki, ref, { kaynakToplam, yuklenen }) {
|
||||
}
|
||||
}
|
||||
|
||||
// 1b) Kalıcı erime: 7 günlük ortalama düşüşe ayak uydurduğu için sustuysa
|
||||
// bile, 30 günlük referans hâlâ çok yukarıdaysa bunu ayrıca bildiririz.
|
||||
if (kaynakToplam != null && ortalama30 > 0) {
|
||||
const oran30 = kaynakToplam / ortalama30;
|
||||
sonuc.aylikOrtalamayaOran = Number(oran30.toFixed(3));
|
||||
if (oran30 < DUSUS_ESIGI_30 && !sonuc.uyari) {
|
||||
sonuc.uyari = `Kaynak havuzu 30 günlük ortalamanın %${Math.round((1 - oran30) * 100)} altında ` +
|
||||
`(kalıcı düşüş): ortalama ${ortalama30}, şimdi ${kaynakToplam} ilan.`;
|
||||
sonuc.tur = 'kalici_dusus';
|
||||
} else if (oran30 < DUSUS_ESIGI_30 && sonuc.uyari) {
|
||||
// Zaten haftalık uyarı var; 30 günlük bağlamı mesaja ekle.
|
||||
sonuc.uyari += ` (30 günlük ortalama ${ortalama30}, oran %${Math.round(oran30 * 100)})`;
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
sonuc.uyari = `Havuz eksik yüklendi: kaynakta ${kaynakToplam}, bizde ${yuklenen} ilan.`;
|
||||
@@ -342,3 +370,35 @@ export function havuzGecmisi(limit = 60) {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Havuz özeti — 7 ve 30 günlük ortalamalar SQL'de hesaplanır; döndürülen
|
||||
* satır sayısından bağımsızdır (60 kayıtlık pencereye sıkışmaz).
|
||||
*/
|
||||
export function havuzOzet() {
|
||||
const db = openDb();
|
||||
if (!db) return null;
|
||||
try {
|
||||
const pencere = (gun) => {
|
||||
const bas = new Date(Date.now() - gun * 864e5).toISOString();
|
||||
const r = db
|
||||
.prepare(`SELECT AVG(kaynak_toplam) ort, MAX(kaynak_toplam) zirve, MIN(kaynak_toplam) dip,
|
||||
COUNT(kaynak_toplam) adet
|
||||
FROM havuz_olcum WHERE zaman >= ? AND kaynak_toplam IS NOT NULL`)
|
||||
.get(bas) || {};
|
||||
return {
|
||||
ortalama: r.ort ? Math.round(r.ort) : null,
|
||||
zirve: r.zirve ?? null,
|
||||
dip: r.dip ?? null,
|
||||
adet: r.adet ?? 0,
|
||||
};
|
||||
};
|
||||
const son = db
|
||||
.prepare('SELECT zaman, kaynak_toplam, yuklenen, eksik, dt, arsiv FROM havuz_olcum ORDER BY id DESC LIMIT 1')
|
||||
.get() || null;
|
||||
return { son, hafta: pencere(7), ay: pencere(30) };
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
+67
-28
@@ -24,7 +24,7 @@ import { billingConfigured, webhookConfigured, planCatalog, createCheckoutSessio
|
||||
import * as store from './user-store.mjs';
|
||||
import { planOf, haklar, kullanim, kotaKontrol, ozellikVar } from './entitlements.mjs';
|
||||
import { searchTed } from './ted.mjs';
|
||||
import { readHistory, historyStats, havuzOlc, havuzGecmisi } from './history-store.mjs';
|
||||
import { readHistory, historyStats, havuzOlc, havuzGecmisi, havuzOzet } from './history-store.mjs';
|
||||
import { startScheduler, schedulerStatus } from './scheduler.mjs';
|
||||
import { searchKik, kararMetni } from './kik.mjs';
|
||||
import { documentUrl, ilanMetni, documentText, browserAvailable } from './documents.mjs';
|
||||
@@ -79,18 +79,40 @@ for (const [region, cities] of Object.entries(REGIONS)) {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Basit sektör / kurum tipi sınıflandırması (başlık ve kurum adına göre)
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sektör sınıflandırması — sıra ÖNEMLİ: ilk eşleşen kazanır, bu yüzden
|
||||
// dar/özel kalıplar geniş olanlardan önce gelir (ör. "tıbbi gaz" → Sağlık,
|
||||
// "doğalgaz" → Elektrik & Enerji).
|
||||
// UYARI: buradaki etiketler src/data/constants.js SECTORS_LIST ile birebir
|
||||
// aynı olmalı. Sürüklenmeyi önlemek için liste /api/meta/sektorler ile sunulur.
|
||||
const SECTOR_RULES = [
|
||||
[/tıbbi|medikal|sağlık|hastane|ilaç|cerrahi|laboratuvar|diş/i, 'Sağlık / Tıbbi Malzeme'],
|
||||
[/yazılım|bilişim|sunucu|donanım|kamera|network|bilgisayar/i, 'Bilişim & Yazılım'],
|
||||
[/gıda|yemek|catering|ekmek|et |sebze|meyve|kumanya/i, 'Gıda & Catering'],
|
||||
[/temizlik|hijyen|dezenfek|deterjan/i, 'Temizlik & Hijyen'],
|
||||
[/inşaat|yapım|onarım|tadilat|bakım|asfalt|beton|sulama|yol /i, 'İnşaat & Bakım'],
|
||||
[/kırtasiye|matbaa|baskı|kağıt|toner/i, 'Kırtasiye & Büro'],
|
||||
[/giyim|tekstil|elbise|üniforma|kıyafet/i, 'Tekstil & Giyim'],
|
||||
[/araç|otomotiv|lastik|akaryakıt|yakıt|kiralama/i, 'Otomotiv & Yedek Parça'],
|
||||
[/elektrik|trafo|enerji|aydınlatma|jeneratör|güneş/i, 'Elektrik & Enerji'],
|
||||
[/tıbbi|medikal|sağlık|hastane|ilaç|cerrahi|laboratuvar|diş |protez|ortez|görüntüleme|röntgen|tomografi|steril|enjektör|eldiven|maske|reaktif|kit alım/i, 'Sağlık / Tıbbi Malzeme'],
|
||||
[/yazılım|bilişim|sunucu|donanım|network|bilgisayar|lisans|veri merkezi|siber|yazıcı alım|tablet|projeksiyon/i, 'Bilişim & Yazılım'],
|
||||
[/güvenlik hizmet|özel güvenlik|kamera|cctv|alarm|turnike|x-ray|güvenlik sistem/i, 'Güvenlik Sistemleri'],
|
||||
[/gıda|yemek|catering|ekmek|et alım|sebze|meyve|kumanya|süt|tavuk|bakliyat|mutfak/i, 'Gıda & Catering'],
|
||||
[/temizlik|hijyen|dezenfek|deterjan|çöp|atık toplama|haşere|ilaçlama/i, 'Temizlik & Hijyen'],
|
||||
[/asfalt|beton|yol yapım|köprü|altyapı|kanalizasyon|içme suyu|sulama|menfez|istinat/i, 'Altyapı & Yol'],
|
||||
[/inşaat|yapım işi|onarım|tadilat|güçlendirme|çatı|cephe|restorasyon|bakım onarım/i, 'İnşaat & Bakım'],
|
||||
[/kırtasiye|matbaa|baskı|kağıt|toner|kartuş|fotokopi|büro malzeme/i, 'Kırtasiye & Büro'],
|
||||
[/mobilya|masa|sandalye|dolap|okul sırası|büro mobilya|raf /i, 'Mobilya & Donatı'],
|
||||
[/giyim|tekstil|elbise|üniforma|kıyafet|ayakkabı|çamaşır|kumaş|battaniye/i, 'Tekstil & Giyim'],
|
||||
[/akaryakıt|motorin|benzin|lpg|yakıt alım/i, 'Akaryakıt & Yakıt'],
|
||||
[/araç kirala|taşıt kirala|servis hizmet|personel taşıma|öğrenci taşıma|nakliye|lojistik/i, 'Taşımacılık & Kiralama'],
|
||||
[/araç|otomotiv|lastik|yedek parça|otobüs|kamyon|iş makinesi|greyder|ekskavatör/i, 'Otomotiv & İş Makinesi'],
|
||||
[/elektrik|trafo|enerji|aydınlatma|jeneratör|güneş|solar|kablo|pano |ups /i, 'Elektrik & Enerji'],
|
||||
[/doğalgaz|kalorifer|kazan|ısıtma|soğutma|klima|havalandırma|iklimlendirme|mekanik tesisat/i, 'Isıtma & İklimlendirme'],
|
||||
[/asansör|yürüyen merdiven/i, 'Asansör & Yürüyen Sistem'],
|
||||
[/su arıtma|arıtma tesis|pompa|sondaj|kuyu |su deposu/i, 'Su & Arıtma'],
|
||||
[/tarım|tohum|gübre|fide|zirai|hayvan|yem |veteriner|sera /i, 'Tarım & Hayvancılık'],
|
||||
[/peyzaj|park bahçe|ağaç|çim |fidan|oyun grubu|kent mobilya/i, 'Peyzaj & Park'],
|
||||
[/eğitim hizmet|kurs|seminer|danışmanlık|müşavirlik|etüt proje|mühendislik hizmet|kontrollük/i, 'Danışmanlık & Proje'],
|
||||
[/reklam|tanıtım|organizasyon|fuar|basın|medya|prodüksiyon/i, 'Reklam & Organizasyon'],
|
||||
[/sigorta|zorunlu trafik|kasko/i, 'Sigorta'],
|
||||
[/kitap|yayın|kütüphane|arşiv|dijitalleştirme/i, 'Yayın & Arşiv'],
|
||||
[/spor|halı saha|tribün|fitness|kondisyon/i, 'Spor Tesisleri'],
|
||||
[/laboratuvar cihaz|analiz cihaz|ölçüm cihaz|test cihaz/i, 'Laboratuvar & Ölçüm'],
|
||||
];
|
||||
|
||||
|
||||
const INSTITUTION_RULES = [
|
||||
[/büyükşehir belediye/i, 'Büyükşehir Belediyeleri'],
|
||||
[/belediye/i, 'Belediyeler'],
|
||||
@@ -102,6 +124,13 @@ const INSTITUTION_RULES = [
|
||||
[/a\.ş\.|genel müdürlüğü|işletme/i, 'Kamu Şirketleri'],
|
||||
];
|
||||
|
||||
// Arayüzün kullandığı sektör listesi buradan türetilir — elle tutulan ikinci
|
||||
// bir liste tutmayız, böylece iki taraf birbirinden ayrı düşemez.
|
||||
const SEKTOR_ETIKETLERI = [...new Set(SECTOR_RULES.map(([, ad]) => ad))].sort((a, b) =>
|
||||
a.localeCompare(b, 'tr'));
|
||||
const KURUM_ETIKETLERI = [...new Set(INSTITUTION_RULES.map(([, ad]) => ad))].sort((a, b) =>
|
||||
a.localeCompare(b, 'tr'));
|
||||
|
||||
function classify(rules, text, fallback) {
|
||||
for (const [re, label] of rules) if (re.test(text)) return label;
|
||||
return fallback;
|
||||
@@ -965,29 +994,39 @@ const server = http.createServer(async (req, res) => {
|
||||
}
|
||||
|
||||
if (url.pathname === '/api/health') return json(res, 200, { ok: true });
|
||||
if (url.pathname === '/api/meta/filtreler') {
|
||||
return json(res, 200, {
|
||||
success: true,
|
||||
sektorler: SEKTOR_ETIKETLERI,
|
||||
kurumTipleri: KURUM_ETIKETLERI,
|
||||
});
|
||||
}
|
||||
|
||||
if (url.pathname === '/api/health/havuz') {
|
||||
const gecmis = havuzGecmisi(Number(url.searchParams.get('limit')) || 60);
|
||||
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 && 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;
|
||||
const ozet = havuzOzet();
|
||||
if (!ozet) return json(res, 200, { success: true, son: null, olcumSayisi: 0, gecmis: [] });
|
||||
const { son, hafta, ay } = ozet;
|
||||
const oran = (ort, yeterli) =>
|
||||
yeterli && son?.kaynak_toplam && ort ? Number((son.kaynak_toplam / ort).toFixed(3)) : null;
|
||||
const yeterliVeri = hafta.adet >= 5;
|
||||
const yeterliVeri30 = ay.adet >= 20;
|
||||
return json(res, 200, {
|
||||
success: true,
|
||||
son,
|
||||
haftalikOrtalama: ortalama,
|
||||
haftalikZirve: zirve || null,
|
||||
olcumAdedi: hafta.length,
|
||||
// 7 gün: günlük dalgalanma
|
||||
haftalikOrtalama: hafta.ortalama,
|
||||
haftalikZirve: hafta.zirve,
|
||||
olcumAdedi: hafta.adet,
|
||||
yeterliVeri,
|
||||
ortalamayaOran:
|
||||
yeterliVeri && son?.kaynak_toplam && ortalama
|
||||
? Number((son.kaynak_toplam / ortalama).toFixed(3))
|
||||
: null,
|
||||
olcumSayisi: gecmis.length,
|
||||
gecmis,
|
||||
ortalamayaOran: oran(hafta.ortalama, yeterliVeri),
|
||||
// 30 gün: kalıcı erime
|
||||
aylikOrtalama: ay.ortalama,
|
||||
aylikZirve: ay.zirve,
|
||||
aylikDip: ay.dip,
|
||||
olcumAdedi30: ay.adet,
|
||||
yeterliVeri30,
|
||||
aylikOrtalamayaOran: oran(ay.ortalama, yeterliVeri30),
|
||||
gecmis: havuzGecmisi(Number(url.searchParams.get('limit')) || 60),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user