fix: ust serit tiklaninca donuyordu — Kanban havuzdaki tum ihaleleri (8 bin+ kart) ciziyordu; artik yalnizca takipteki/kolona tasinan ihaleler, sekme gecisi transition ile aninda vurgulaniyor

- KanbanBoard: sunucu her ilana varsayilan 'incelenecek' verdigi icin bos pano 8.372 kart basiyordu; yalnizca takipte veya kolona tasinmis ihaleler kaliyor
- TopNavigation: useTransition + spinner; agir sekmelerde tiklama karsiliksiz kalmiyor
- sunucu: 'yeni liste eskisinden kucukse alma' kurali havuzu 30 Agustos zirvesinde dondurmustu (gunlerce yeni ilan yok); eksiksiz inen liste her zaman devrede
- sunucu: isYeni/bugun eklenen UTC-TR gun farkindan hep 0'di; Europe/Istanbul gunune gore ve her yanitta tazeleniyor (eklenmeZamani da donmuyor)
- ust kartlar: 'bugun eklenenler' tum ilanlari sayiyordu ('once' metnine bakiyordu), 'son giristen beri' sabit 14'tu; sayimlar liste filtresiyle ayni olcut (ihaleTarih.js), arsiv/sonuclanan filtreleri 'sadece acik' kapisina takilmiyor

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 13:02:40 +03:00
co-authored by Claude Fable 5.1
parent 3eb0f91823
commit 920a147126
9 changed files with 226 additions and 56 deletions
+52 -11
View File
@@ -109,6 +109,33 @@ function relativeTimeTr(iso) {
return `${Math.floor(hours / 24)} gün önce eklendi`;
}
// Türkiye günü ("yyyy-aa-gg"). Sunucu konteyneri UTC'de çalışıyor; ilan.gov.tr
// yayın saati TR gece yarısı = "…T21:00:00Z" olduğundan UTC'ye göre gün DÜN
// çıkıyor ve "bugün eklenen" hep 0 görünüyordu.
const TR_GUN = new Intl.DateTimeFormat('en-CA', {
timeZone: 'Europe/Istanbul', year: 'numeric', month: '2-digit', day: '2-digit',
});
function trGun(iso) {
if (!iso) return '';
const d = new Date(iso);
return Number.isNaN(d.getTime()) ? String(iso).slice(0, 10) : TR_GUN.format(d);
}
const bugunTR = () => TR_GUN.format(new Date());
// isYeni / eklenmeZamani ilan havuza girerken hesaplanıp donuyordu; havuz
// günlerce yaşayınca "22 saat önce" damgası ve "bugün" bayrağı bayatlıyordu.
// Her yanıtta tazelenir (3 bin kayıt için birkaç ms).
function ilanTazele(t) {
if (!t.isLiveFeed || t.kaynak !== 'ilan.gov.tr') return t;
t.isYeni = Boolean(t.ilanTarihi) && t.ilanTarihi === bugunTR();
if (t.yayinZamani) t.eklenmeZamani = relativeTimeTr(t.yayinZamani);
return t;
}
const bugunEklenenSayisi = (liste) => {
const bugun = bugunTR();
return (liste || []).filter((t) => t.kaynak === 'ilan.gov.tr' && t.ilanTarihi === bugun).length;
};
function daysUntil(ddmmyyyy) {
if (!ddmmyyyy) return null;
const [d, m, y] = ddmmyyyy.split('.').map(Number);
@@ -130,8 +157,7 @@ function mapAd(ad) {
let aciliyetBadge = null;
if (kalanGun !== null && kalanGun >= 0 && kalanGun <= 3) aciliyetBadge = '⚠️ SON 3 GÜN';
const publishedToday =
ad.publishStartDate && new Date(ad.publishStartDate).toDateString() === new Date().toDateString();
const ilanTarihi = trGun(ad.publishStartDate);
return {
id: `ilan-${ad.id}`,
@@ -151,8 +177,9 @@ function mapAd(ad) {
brans: ihaleTuru || 'Genel',
kurumTipi: classify(INSTITUTION_RULES, ad.advertiserName || '', 'Diğer Kamu Kurumları'),
eklenmeZamani: relativeTimeTr(ad.publishStartDate),
ilanTarihi: ad.publishStartDate ? String(ad.publishStartDate).slice(0, 10) : '',
isYeni: Boolean(publishedToday),
yayinZamani: ad.publishStartDate || null,
ilanTarihi,
isYeni: Boolean(ilanTarihi) && ilanTarihi === bugunTR(),
isLiveFeed: true,
aciliyetBadge,
items: [],
@@ -259,7 +286,7 @@ async function ilanSayfalari(baslangic, adet) {
istekler.push(
ilanSearch({ ats: [3] }, page * LIST_SIZE, LIST_SIZE).catch((e) => {
console.warn(`[ihaleist-api] sayfa ${page} alınamadı: ${e.message}`);
return { ads: [] };
return { ads: [], hata: true };
})
);
}
@@ -366,20 +393,31 @@ async function arkaPlandaTamamla(numFound, ilkListe) {
const yeni = [...ilkListe];
const gerekliSayfa = Math.min(MAX_PAGES, Math.ceil(numFound / LIST_SIZE));
let page = ILK_PARTI;
let eksiksiz = true; // her sayfa hatasız indi mi?
while (page < gerekliSayfa) {
const grup = Math.min(ES_ZAMANLI, gerekliSayfa - page);
const sonuclar = await ilanSayfalari(page, grup);
if (sonuclar.some((r) => r.hata)) eksiksiz = false;
const ads = sonuclar.flatMap((r) => r.ads || []);
if (!ads.length) break;
if (!ads.length) { eksiksiz = false; break; }
const eklenen = ilanlariBirlestir(yeni, ads);
if (eklenen.length) yeni.push(...eklenen);
page += grup;
await new Promise((r) => setTimeout(r, 120)); // kaynağı yormamak için
}
// Yalnızca daha zengin bir liste elde ettiysek devreye al
if (yeni.length >= cache.tenders.length) {
// EKSİKSİZ inen liste her zaman devreye girer — kaynaktaki ihale sayısı
// düşse bile. Eskiden "yeni liste eskisinden küçükse alma" kuralı vardı;
// kaynak 3.372'lik zirveden 3.100'e inince havuz o zirve anında DONDU:
// günlerce yeni ilan eklenmedi, "bugün eklenen" 0 kaldı. Yarım inen liste
// ise yalnızca eldekinden zenginse alınır (kullanıcı eksik listeye düşmesin).
if (eksiksiz || yeni.length >= cache.tenders.length) {
if (yeni.length < cache.tenders.length) {
console.log(`[ihaleist-api] havuz küçüldü: ${cache.tenders.length} → ${yeni.length} (kaynak ${numFound}); güncel liste devrede`);
}
cache.tenders = yeni;
cache.todayCount = yeni.filter((t) => t.isYeni).length;
cache.todayCount = bugunEklenenSayisi(yeni);
} else {
console.warn(`[ihaleist-api] yenileme eksik indi (${yeni.length}/${numFound}); eldeki ${cache.tenders.length} ilan korunuyor`);
}
console.log(`[ihaleist-api] havuz hazır: ${cache.tenders.length}/${numFound} ilan`);
havuzKaydet(numFound, cache.tenders.length);
@@ -513,7 +551,7 @@ async function getTenders(force = false) {
// Yenileme: eldeki dolu listeyi KORU, yenisi arka planda hazırlansın.
if (!cache.tenders || cache.tenders.length === 0) {
cache.tenders = ilkListe;
cache.todayCount = ilkListe.filter((t) => t.isYeni).length;
cache.todayCount = bugunEklenenSayisi(ilkListe);
console.log(`[ihaleist-api] ilk yükleme: ${ilkListe.length} ilan (havuz ${cache.totalCount}) — kalanı arka planda`);
} else {
console.log(`[ihaleist-api] yenileme başladı (mevcut ${cache.tenders.length} ilan sunulmaya devam ediyor)`);
@@ -589,6 +627,9 @@ async function tendersPayload(force = false) {
if (dt.available) sources.push('EKAP Doğrudan Temin (22/d) worker');
if (hist.available && hist.totalCount) sources.push('EKAP Sonuç İlanları (geçmiş sonuçlar)');
for (const t of c.tenders) ilanTazele(t); // isYeni + "x saat önce" gün değişse de doğru
c.todayCount = bugunEklenenSayisi(c.tenders);
return {
success: true,
source: sources.join(' + '),
@@ -1107,7 +1148,7 @@ const server = http.createServer(async (req, res) => {
return json(res, 200, {
success: true,
toplamIlan: c.totalCount,
bugunEklenen: c.todayCount,
bugunEklenen: bugunEklenenSayisi(c.tenders),
yuklenenIlan: c.tenders.length,
kaynak: 'ilan.gov.tr',
});