From 21e80d0dd6c94d4e7c95c22179cb50fcbbb2643c Mon Sep 17 00:00:00 2001 From: ht Date: Sun, 6 Sep 2026 10:31:58 +0300 Subject: [PATCH] Expose partial item search failures and allow query-scoped retries --- docs/quality/uygulama-durumu.md | 8 ++++++++ src/components/TenderList.jsx | 7 ++++++- src/context/AppContext.jsx | 17 +++++++++++------ src/services/kalem.js | 20 +++++++++++++++----- tests/shared-items.test.mjs | 13 +++++++++++++ 5 files changed, 53 insertions(+), 12 deletions(-) diff --git a/docs/quality/uygulama-durumu.md b/docs/quality/uygulama-durumu.md index 18f2b48..39266f6 100644 --- a/docs/quality/uygulama-durumu.md +++ b/docs/quality/uygulama-durumu.md @@ -587,3 +587,11 @@ SQLite desteği olmayan ortamda ortak kalem deposu artık boş liste gibi görü `IHALEIST_SHARED_DATA_DIR` ile ortak kalem deposu dizini yapılandırılabilir. Tarayıcı QA ve HTTP test sunucuları ortak kalem verisini kendi geçici dizinlerinde tutacak şekilde güncellendi. Önceden başlatılmış test süreçleri yeniden başlatılmadı; mevcut veri dosyaları silinmedi. Diğer ortak veri depolarının tamamı bu değişiklikle izole edilmiş sayılmaz. Test: bozuk gerçek SQLite dosyasıyla açılış başarısızlığı, dosyanın onarılmasından sonra aynı süreçte boş liste ve yeni kalem yazma/okuma. Tüm 128 test ve üretim derlemesi geçti. HTTP testinin yeni izole depo ayarı sonrasında dört HTTP kabul testi ayrıca geçti. Canlı sistemde yapay arıza oluşturulmadı. Büyük paket uyarıları ve planın kalan kapsamı açık; canlı yayın yapılmadı. + +### D05 — Ana aramada kalem kaynağı durumu + +Ortak kalem araması başarısız olduğunda istemci boş dizi, AppContext ise sessiz boş durum üretiyordu. Artık servis HTTP/geçersiz yanıt hatalarını ayrı döndürür; istek iptal edilebilir ve otuz saniyede zaman aşımına uğrar. Ana aramanın kalem sonuçları sorguya bağlı yükleniyor/tamamlandı/hata durumları taşır. Sorgu değişikliği önceki isteği iptal eder; geç yanıt/hata yeni sorguya atanmaz. + +Liste kalem araması sürerken sonuçların genişleyebileceğini, hata olduğunda yalnız başlık/kurum/yüklü kart aramasıyla devam edildiğini ve sonuçların eksik olabileceğini açıklar. Kalem aramasını yeniden deneme düğmesi eklendi. Gerçek boş kalem sonucu hata sayılmaz. + +Doğrulama: boş başarılı sonuç, depo 503 hatası ve iptal edilmiş arama için servis testi. Mevcut sorgu eşleşmesi testleriyle birlikte tüm 129 test ve üretim derlemesi geçti. Yeni hata durumunun tarayıcı kabulü açık. Büyük paket uyarıları sürüyor; canlı yayın yapılmadı. diff --git a/src/components/TenderList.jsx b/src/components/TenderList.jsx index b625f7a..76374d7 100644 --- a/src/components/TenderList.jsx +++ b/src/components/TenderList.jsx @@ -16,7 +16,7 @@ export const TenderList = () => { const { tenders, searchQuery, - kalemEslesen, + kalemEslesen, itemSearchState, retryItemSearch, uyumEsigi, selectedCity, selectedAlimTuru, @@ -174,6 +174,11 @@ export const TenderList = () => { {sortBy === 'newest' &&

Yayın tarihi bilinmeyen kayıtlar sonda gösterilir. Aynı gün içinde saati bilinen kayıtlar önce gelir.

} + {itemSearchState?.status === 'loading' &&

Doküman kalemlerinde arama sürüyor; listedeki sonuçlar henüz genişleyebilir.

} + {itemSearchState?.status === 'error' &&
+

{itemSearchState.error} Başlık, kurum ve yüklü kart bilgileriyle arama sürüyor; sonuçlar eksik olabilir.

+ +
} {preparedExport && preparedExport.session === getToken() &&

{preparedExport.createdAt} itibarıyla {preparedExport.count} ihale için Excel hazır{preparedExport.query ? ` — arama: ${preparedExport.query}` : ''}.

Excel dosyasını indir — {preparedExport.name} diff --git a/src/context/AppContext.jsx b/src/context/AppContext.jsx index ba3a2c9..7c413e8 100644 --- a/src/context/AppContext.jsx +++ b/src/context/AppContext.jsx @@ -58,21 +58,26 @@ export const AppProvider = ({ children }) => { // ilk birkaç kalem taşınıyor, tam arama sunucuda yapılıyor. const [kalemSonucu, setKalemSonucu] = useState(null); const kalemEslesen = matchingItemResults(kalemSonucu, searchQuery); + const [kalemRetry,setKalemRetry] = useState(0); + const retryItemSearch = () => setKalemRetry(n=>n+1); + const itemSearchState = kalemSonucu?.query === normalizeTenderSearch(searchQuery) ? kalemSonucu : null; useEffect(() => { const q = searchQuery.trim(); if (q.length < 2) { setKalemSonucu(null); return; } let iptal = false; + const controller=new AbortController(); + setKalemSonucu({query:normalizeTenderSearch(q),status:'loading',matches:null}); const zaman = setTimeout(() => { import('../services/kalem') - .then(({ kalemAra }) => kalemAra(q)) + .then(({ kalemAra }) => kalemAra(q,{signal:controller.signal})) .then((sonuclar) => { if (iptal) return; - setKalemSonucu({query:normalizeTenderSearch(q),matches:sonuclar.length ? new Map(sonuclar.map((s) => [s.ikn, s.eslesen])) : null}); + setKalemSonucu({query:normalizeTenderSearch(q),status:'loaded',matches:sonuclar.length ? new Map(sonuclar.map((s) => [s.ikn, s.eslesen])) : null}); }) - .catch(() => { if (!iptal) setKalemSonucu(null); }); + .catch(error => { if (!iptal) setKalemSonucu({query:normalizeTenderSearch(q),status:'error',error:error.message,matches:null}); }); }, 350); - return () => { iptal = true; clearTimeout(zaman); }; - }, [searchQuery]); + return () => { iptal = true; controller.abort(); clearTimeout(zaman); }; + }, [searchQuery,kalemRetry]); const [selectedCity, setSelectedCity] = useState(''); const [selectedAlimTuru, setSelectedAlimTuru] = useState(''); const [selectedKurumTipi, setSelectedKurumTipi] = useState(''); @@ -725,7 +730,7 @@ export const AppProvider = ({ children }) => { handleLogout, liveStats, searchQuery, - kalemEslesen, + kalemEslesen, itemSearchState, retryItemSearch, setSearchQuery, selectedCity, setSelectedCity, diff --git a/src/services/kalem.js b/src/services/kalem.js index 074e257..00f8dca 100644 --- a/src/services/kalem.js +++ b/src/services/kalem.js @@ -26,11 +26,21 @@ export async function kalemleriGetir(ikn, {signal, timeoutMs = 30000} = {}) { } /** Kalem metninde arama — eşleşen İKN'ler. */ -export async function kalemAra(q) { - const r = await fetch(`/api/kalem/ara?q=${encodeURIComponent(q)}`); - const d = await r.json().catch(() => ({})); - if (!d.success) return []; - return d.sonuclar || []; +export async function kalemAra(q,{signal,timeoutMs=30000}={}) { + const controller=new AbortController(); + const cancel=()=>controller.abort(); + if(signal?.aborted)cancel();else signal?.addEventListener('abort',cancel,{once:true}); + let timedOut=false; + const timer=setTimeout(()=>{timedOut=true;controller.abort();},timeoutMs); + try { + const r=await fetch(`/api/kalem/ara?q=${encodeURIComponent(q)}`,{signal:controller.signal}); + const d=await r.json().catch(()=>null); + if(!r.ok || d?.success!==true || !Array.isArray(d.sonuclar))throw new Error('Doküman kalemlerinde arama tamamlanamadı.'); + return d.sonuclar; + } catch(error) { + if(timedOut)throw new Error('Doküman kalemlerinde arama zaman aşımına uğradı.'); + throw error; + } finally {clearTimeout(timer);signal?.removeEventListener('abort',cancel);} } /** diff --git a/tests/shared-items.test.mjs b/tests/shared-items.test.mjs index 561dd42..69ccb59 100644 --- a/tests/shared-items.test.mjs +++ b/tests/shared-items.test.mjs @@ -23,3 +23,16 @@ test('shared inventory waits are bounded and can be cancelled on tender change', await assert.rejects(pending,{name:'AbortError'}); } finally {globalThis.fetch=original;} }); +test('item search does not convert unavailable inventory into zero matches',async()=>{ + const {kalemAra}=await import('../src/services/kalem.js'); + const original=globalThis.fetch; + try { + globalThis.fetch=async()=>Response.json({success:true,sonuclar:[]}); + assert.deepEqual(await kalemAra('tüp'),[]); + globalThis.fetch=async()=>Response.json({success:false,error:'storage offline'},{status:503}); + await assert.rejects(kalemAra('tüp'),/arama tamamlanamadı/); + const c=new AbortController();c.abort(); + globalThis.fetch=async(url,{signal})=>{signal.throwIfAborted();}; + await assert.rejects(kalemAra('tüp',{signal:c.signal}),{name:'AbortError'}); + } finally {globalThis.fetch=original;} +});