Persist client document read failures with stale-state protection

This commit is contained in:
2026-09-06 09:36:47 +03:00
parent a109c2e548
commit ba1f93bd76
6 changed files with 52 additions and 3 deletions
+6
View File
@@ -301,3 +301,9 @@ Sınırlar: Bu paket tam belge yaşam döngüsü kabulü değildir. Tarayıcıda
İhale detayında okunabilir, erişilebilir ihale dosyalarına yeniden kalem çıkarımı eklendi. Aynı dosya indirilip metni çıkarılıyor veya mevcut sunucu metin çıkarıcısı kullanılıyor; aynı documentId ile yetkili kalem aktarımı yeniden çağrılıyor. Yeni dosya yüklemesi gerekmiyor. İşlem sırasında silme engelleniyor; sonuç veya hata görünür, belge listesi yeniden okunuyor. Liste okuma hatası artık sessizce yutulmuyor; yükleniyor, hata ve yeniden yükle durumları var. Eski liste isteğinin yeni ihale listesini ezmesi istek numarasıyla önleniyor. Belge satırları dar alanda sarılabiliyor. İhale detayında okunabilir, erişilebilir ihale dosyalarına yeniden kalem çıkarımı eklendi. Aynı dosya indirilip metni çıkarılıyor veya mevcut sunucu metin çıkarıcısı kullanılıyor; aynı documentId ile yetkili kalem aktarımı yeniden çağrılıyor. Yeni dosya yüklemesi gerekmiyor. İşlem sırasında silme engelleniyor; sonuç veya hata görünür, belge listesi yeniden okunuyor. Liste okuma hatası artık sessizce yutulmuyor; yükleniyor, hata ve yeniden yükle durumları var. Eski liste isteğinin yeni ihale listesini ezmesi istek numarasıyla önleniyor. Belge satırları dar alanda sarılabiliyor.
Doğrulama: üretim derlemesi geçti; HTTP testi, başarısız çıkarım ve yeniden deneme sonrasında aynı belge kaydının ve orijinal dosya baytlarının korunduğunu kontrol ediyor. Bu düğmenin tarayıcı kabulü, genel Doküman Deposu'na aynı işlemin taşınması ve tarayıcı metin okuma hatasının sunucuda kalıcılaştırılması henüz tamamlanmadı. Doğrulama: üretim derlemesi geçti; HTTP testi, başarısız çıkarım ve yeniden deneme sonrasında aynı belge kaydının ve orijinal dosya baytlarının korunduğunu kontrol ediyor. Bu düğmenin tarayıcı kabulü, genel Doküman Deposu'na aynı işlemin taşınması ve tarayıcı metin okuma hatasının sunucuda kalıcılaştırılması henüz tamamlanmadı.
### Tarayıcı metin okuma hatasının kalıcı aktarımı
İhale detayındaki yükleme ve yeniden kalem çıkarımı, dosya metni okunamadığında sahiplik kontrollü read-error uç noktasına hata bildiriyor. Sunucu serbest hata metni veya başarı durumu kabul etmiyor; sabit, kullanıcıya yönelik okuma hatasını saklıyor. İstemcinin gördüğü önceki işleme durumu atomik UPDATE koşuluyla karşılaştırılıyor; gecikmiş hata bildirimi daha yeni sonucu ezemiyor (409). Bildirim kaydedilemezse arayüz bunu ayrıca belirtiyor.
70 test geçti. HTTP kapsamında oturumsuz/başka hesap/firma belgesi reddi, kalıcı hata ve eski durumla tekrar bildirimin 409 olması doğrulandı. Mevcut dosya ve baytları sonraki çıkarım denemelerinde korunuyor. Gerçek PDF ile tarayıcı kabulü ve genel depoya yeniden işleme entegrasyonu açık; bu değişiklik tam D01 kabulü sayılmıyor.
+13
View File
@@ -1064,6 +1064,19 @@ const server = http.createServer(async (req, res) => {
return json(res, 200, { success: true, belgeler: store.listDocuments(user.email) }); return json(res, 200, { success: true, belgeler: store.listDocuments(user.email) });
} catch (e) { return json(res, 400, { success: false, error: e.message }); } } catch (e) { return json(res, 400, { success: false, error: e.message }); }
} }
if (url.pathname === '/api/me/documents/read-error' && req.method === 'POST') {
const user = userFromAuthHeader(req.headers['authorization']);
if (!user) return json(res,401,{success:false,error:'oturum yok'});
try {
const body = await readBody(req,8192);
const rec = body.id && store.getDocument(user.email,String(body.id));
if (!rec || rec.tur !== 'ihale') return json(res,404,{success:false,error:'ihale dokümanı bulunamadı'});
if (!Object.hasOwn(body,'expectedProcessing')) return json(res,400,{success:false,error:'işleme sürümü gerekli'});
if (!store.reportDocumentReadError(user.email,rec.id,body.expectedProcessing))
return json(res,409,{success:false,error:'Belge işleme durumu değişti. Listeyi yeniden yükleyin.'});
return json(res,200,{success:true});
} catch (e) { return json(res,400,{success:false,error:e.message}); }
}
// Belge metni — docx/xlsx/zip için. PDF tarayıcıda (pdfjs) okunuyor; // Belge metni — docx/xlsx/zip için. PDF tarayıcıda (pdfjs) okunuyor;
// bu türler için tarayıcıda çözücü yok, baytlar zaten burada duruyor. // bu türler için tarayıcıda çözücü yok, baytlar zaten burada duruyor.
if (url.pathname === '/api/me/documents/text') { if (url.pathname === '/api/me/documents/text') {
+6
View File
@@ -284,6 +284,12 @@ export function setDocumentProcessing(email,id,processing) {
.run(JSON.stringify({...processing,updatedAt:now()}),id,email).changes>0); .run(JSON.stringify({...processing,updatedAt:now()}),id,email).changes>0);
} }
export function reportDocumentReadError(email,id,expectedProcessing) {
const processing={status:'error',error:'Dosya metni okunamadı. Dosya biçimini ve bütünlüğünü kontrol edip yeniden deneyin.',updatedAt:now()};
return withDb(db=>db.prepare("UPDATE belgeler SET isleme=? WHERE id=? AND email=? AND tur='ihale' AND isleme IS ?")
.run(JSON.stringify(processing),id,email,expectedProcessing == null ? null : JSON.stringify(expectedProcessing)).changes>0);
}
export function getDocument(email, id) { export function getDocument(email, id) {
return withDb((db) => db.prepare('SELECT * FROM belgeler WHERE id=? AND email=?').get(id, email) || null); return withDb((db) => db.prepare('SELECT * FROM belgeler WHERE id=? AND email=?').get(id, email) || null);
} }
+15 -3
View File
@@ -3,7 +3,7 @@ import React, { useState, useEffect, useRef, useCallback } from 'react';
import { useApp } from '../context/AppContext'; import { useApp } from '../context/AppContext';
import { getToken } from '../services/auth'; import { getToken } from '../services/auth';
import { FileText, ExternalLink, Loader2, Sparkles, AlertTriangle, Download, Upload, Trash2, Paperclip, Route } from 'lucide-react'; import { FileText, ExternalLink, Loader2, Sparkles, AlertTriangle, Download, Upload, Trash2, Paperclip, Route } from 'lucide-react';
import { uploadDocument, listTenderDocuments, deleteDocument } from '../services/account'; import { uploadDocument, listTenderDocuments, deleteDocument, reportDocumentReadError } from '../services/account';
import { extractText } from '../services/pdfText'; import { extractText } from '../services/pdfText';
/** /**
@@ -65,7 +65,13 @@ export const TenderDocuments = ({ tender }) => {
let kalemHata = cevap.processing?.status === 'error' ? cevap.processing.error : null; let kalemHata = cevap.processing?.status === 'error' ? cevap.processing.error : null;
if (!kalem && ikn && /\.(pdf|txt|md)$/i.test(f.name)) { if (!kalem && ikn && /\.(pdf|txt|md)$/i.test(f.name)) {
try { try {
const { text } = await extractText(f); let text;
try { ({text} = await extractText(f)); }
catch (readError) {
try { await reportDocumentReadError(cevap.id,cevap.belgeler.find(b=>b.id===cevap.id)?.isleme); }
catch { throw new Error(`${readError.message} (hata durumu kaydedilemedi; listeyi yenileyin)`); }
throw readError;
}
const { kalemCikar } = await import('../services/kalem'); const { kalemCikar } = await import('../services/kalem');
const d = await kalemCikar({ ikn, metin: text, dosya: f.name, documentId:cevap.id }); const d = await kalemCikar({ ikn, metin: text, dosya: f.name, documentId:cevap.id });
kalem = d.sayi || null; kalem = d.sayi || null;
@@ -111,7 +117,13 @@ export const TenderDocuments = ({ tender }) => {
const kalemleriYenidenCikar = async (b) => { const kalemleriYenidenCikar = async (b) => {
setBusy('kalem'); setError(null); setBilgi(null); setBusy('kalem'); setError(null); setBilgi(null);
try { try {
const metin = await belgeMetni(b); let metin;
try { metin = await belgeMetni(b); }
catch (readError) {
try { await reportDocumentReadError(b.id,b.isleme); }
catch { throw new Error(`${readError.message} (hata durumu kaydedilemedi; listeyi yenileyin)`); }
throw readError;
}
const { kalemCikar } = await import('../services/kalem'); const { kalemCikar } = await import('../services/kalem');
const result = await kalemCikar({ ikn, metin, dosya:b.ad, documentId:b.id }); const result = await kalemCikar({ ikn, metin, dosya:b.ad, documentId:b.id });
setBilgi({ isleme: `${b.ad}: ${result.sayi || 0} kalem çıkarıldı.` }); setBilgi({ isleme: `${b.ad}: ${result.sayi || 0} kalem çıkarıldı.` });
+3
View File
@@ -106,3 +106,6 @@ export const bildirimOkundu = (idler) => iste('/api/me/bildirimler', { method: '
export const getTenderStates = () => iste('/api/me/tender-state').then(d => d.records || {}); export const getTenderStates = () => iste('/api/me/tender-state').then(d => d.records || {});
export const saveTenderState = (key, patch, snapshot, version) => export const saveTenderState = (key, patch, snapshot, version) =>
iste('/api/me/tender-state', {method:'PUT',body:JSON.stringify({key,patch,snapshot,version})}).then(d => d.record); iste('/api/me/tender-state', {method:'PUT',body:JSON.stringify({key,patch,snapshot,version})}).then(d => d.record);
export const reportDocumentReadError = (id, expectedProcessing) =>
iste('/api/me/documents/read-error', { method:'POST', body:JSON.stringify({id,expectedProcessing:expectedProcessing ?? null}) });
+9
View File
@@ -87,6 +87,15 @@ test('shared item extraction requires an owned tender document and matching tend
assert.equal((await request('/api/kalem/cikar','POST',extract(tender.data.id),other)).status,403); assert.equal((await request('/api/kalem/cikar','POST',extract(tender.data.id),other)).status,403);
assert.equal((await request('/api/kalem/cikar','POST',{...extract(tender.data.id),ikn:'2026/999999'})).status,403); assert.equal((await request('/api/kalem/cikar','POST',{...extract(tender.data.id),ikn:'2026/999999'})).status,403);
assert.equal((await processing()).status,'awaiting_text'); assert.equal((await processing()).status,'awaiting_text');
const prior=await processing();
const readError={id:tender.data.id,expectedProcessing:prior};
assert.equal((await request('/api/me/documents/read-error','POST',readError,null)).status,401);
assert.equal((await request('/api/me/documents/read-error','POST',readError,other)).status,404);
assert.equal((await request('/api/me/documents/read-error','POST',{id:company.data.id,expectedProcessing:null})).status,404);
assert.equal((await request('/api/me/documents/read-error','POST',readError)).status,200);
assert.equal((await processing()).status,'error');
assert.equal((await request('/api/me/documents/read-error','POST',readError)).status,409);
assert.equal((await request('/api/kalem/cikar','POST',{...extract(tender.data.id),metin:{invalid:true}})).status,400); assert.equal((await request('/api/kalem/cikar','POST',{...extract(tender.data.id),metin:{invalid:true}})).status,400);
assert.equal((await processing()).status,'error'); assert.equal((await processing()).status,'error');
assert.equal((await request('/api/kalem/cikar','POST',extract(tender.data.id))).status,200); assert.equal((await request('/api/kalem/cikar','POST',extract(tender.data.id))).status,200);