Expose partial item search failures and allow query-scoped retries
This commit is contained in:
@@ -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.
|
`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ı.
|
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ı.
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const TenderList = () => {
|
|||||||
const {
|
const {
|
||||||
tenders,
|
tenders,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
kalemEslesen,
|
kalemEslesen, itemSearchState, retryItemSearch,
|
||||||
uyumEsigi,
|
uyumEsigi,
|
||||||
selectedCity,
|
selectedCity,
|
||||||
selectedAlimTuru,
|
selectedAlimTuru,
|
||||||
@@ -174,6 +174,11 @@ export const TenderList = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{sortBy === 'newest' && <p className="text-xs text-slate-500">Yayın tarihi bilinmeyen kayıtlar sonda gösterilir. Aynı gün içinde saati bilinen kayıtlar önce gelir.</p>}
|
{sortBy === 'newest' && <p className="text-xs text-slate-500">Yayın tarihi bilinmeyen kayıtlar sonda gösterilir. Aynı gün içinde saati bilinen kayıtlar önce gelir.</p>}
|
||||||
|
{itemSearchState?.status === 'loading' && <p role="status" className="text-xs text-slate-600">Doküman kalemlerinde arama sürüyor; listedeki sonuçlar henüz genişleyebilir.</p>}
|
||||||
|
{itemSearchState?.status === 'error' && <div role="alert" className="rounded-lg border border-amber-200 bg-amber-50 p-3 text-xs text-amber-900">
|
||||||
|
<p>{itemSearchState.error} Başlık, kurum ve yüklü kart bilgileriyle arama sürüyor; sonuçlar eksik olabilir.</p>
|
||||||
|
<button type="button" onClick={retryItemSearch} className="mt-1 underline font-semibold">Kalem aramasını yeniden dene</button>
|
||||||
|
</div>}
|
||||||
{preparedExport && preparedExport.session === getToken() && <div role="status" className="rounded-lg border border-emerald-200 bg-emerald-50 p-3 text-xs space-y-1">
|
{preparedExport && preparedExport.session === getToken() && <div role="status" className="rounded-lg border border-emerald-200 bg-emerald-50 p-3 text-xs space-y-1">
|
||||||
<p>{preparedExport.createdAt} itibarıyla {preparedExport.count} ihale için Excel hazır{preparedExport.query ? ` — arama: ${preparedExport.query}` : ''}.</p>
|
<p>{preparedExport.createdAt} itibarıyla {preparedExport.count} ihale için Excel hazır{preparedExport.query ? ` — arama: ${preparedExport.query}` : ''}.</p>
|
||||||
<a href={preparedExport.url} download={preparedExport.name} className="font-bold text-emerald-800 underline">Excel dosyasını indir — {preparedExport.name}</a>
|
<a href={preparedExport.url} download={preparedExport.name} className="font-bold text-emerald-800 underline">Excel dosyasını indir — {preparedExport.name}</a>
|
||||||
|
|||||||
@@ -58,21 +58,26 @@ export const AppProvider = ({ children }) => {
|
|||||||
// ilk birkaç kalem taşınıyor, tam arama sunucuda yapılıyor.
|
// ilk birkaç kalem taşınıyor, tam arama sunucuda yapılıyor.
|
||||||
const [kalemSonucu, setKalemSonucu] = useState(null);
|
const [kalemSonucu, setKalemSonucu] = useState(null);
|
||||||
const kalemEslesen = matchingItemResults(kalemSonucu, searchQuery);
|
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(() => {
|
useEffect(() => {
|
||||||
const q = searchQuery.trim();
|
const q = searchQuery.trim();
|
||||||
if (q.length < 2) { setKalemSonucu(null); return; }
|
if (q.length < 2) { setKalemSonucu(null); return; }
|
||||||
let iptal = false;
|
let iptal = false;
|
||||||
|
const controller=new AbortController();
|
||||||
|
setKalemSonucu({query:normalizeTenderSearch(q),status:'loading',matches:null});
|
||||||
const zaman = setTimeout(() => {
|
const zaman = setTimeout(() => {
|
||||||
import('../services/kalem')
|
import('../services/kalem')
|
||||||
.then(({ kalemAra }) => kalemAra(q))
|
.then(({ kalemAra }) => kalemAra(q,{signal:controller.signal}))
|
||||||
.then((sonuclar) => {
|
.then((sonuclar) => {
|
||||||
if (iptal) return;
|
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);
|
}, 350);
|
||||||
return () => { iptal = true; clearTimeout(zaman); };
|
return () => { iptal = true; controller.abort(); clearTimeout(zaman); };
|
||||||
}, [searchQuery]);
|
}, [searchQuery,kalemRetry]);
|
||||||
const [selectedCity, setSelectedCity] = useState('');
|
const [selectedCity, setSelectedCity] = useState('');
|
||||||
const [selectedAlimTuru, setSelectedAlimTuru] = useState('');
|
const [selectedAlimTuru, setSelectedAlimTuru] = useState('');
|
||||||
const [selectedKurumTipi, setSelectedKurumTipi] = useState('');
|
const [selectedKurumTipi, setSelectedKurumTipi] = useState('');
|
||||||
@@ -725,7 +730,7 @@ export const AppProvider = ({ children }) => {
|
|||||||
handleLogout,
|
handleLogout,
|
||||||
liveStats,
|
liveStats,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
kalemEslesen,
|
kalemEslesen, itemSearchState, retryItemSearch,
|
||||||
setSearchQuery,
|
setSearchQuery,
|
||||||
selectedCity,
|
selectedCity,
|
||||||
setSelectedCity,
|
setSelectedCity,
|
||||||
|
|||||||
+15
-5
@@ -26,11 +26,21 @@ export async function kalemleriGetir(ikn, {signal, timeoutMs = 30000} = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Kalem metninde arama — eşleşen İKN'ler. */
|
/** Kalem metninde arama — eşleşen İKN'ler. */
|
||||||
export async function kalemAra(q) {
|
export async function kalemAra(q,{signal,timeoutMs=30000}={}) {
|
||||||
const r = await fetch(`/api/kalem/ara?q=${encodeURIComponent(q)}`);
|
const controller=new AbortController();
|
||||||
const d = await r.json().catch(() => ({}));
|
const cancel=()=>controller.abort();
|
||||||
if (!d.success) return [];
|
if(signal?.aborted)cancel();else signal?.addEventListener('abort',cancel,{once:true});
|
||||||
return d.sonuclar || [];
|
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);}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -23,3 +23,16 @@ test('shared inventory waits are bounded and can be cancelled on tender change',
|
|||||||
await assert.rejects(pending,{name:'AbortError'});
|
await assert.rejects(pending,{name:'AbortError'});
|
||||||
} finally {globalThis.fetch=original;}
|
} 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;}
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user