fix: clear stale weekly preference after concurrent changes

This commit is contained in:
2026-09-06 14:44:25 +03:00
parent 38227862c0
commit 8faf03e632
3 changed files with 17 additions and 2 deletions
+7
View File
@@ -1101,3 +1101,10 @@ Taslak türü seçicisine erişilebilir ad eklendi. Üretim derlemesi geçti. Ö
- Sekmeye/pencereye geri dönüldüğünde izin durumu yeniden okunuyor. İzin isteme beklerken ikinci çağrı engelleniyor; kapanmış ekran güncellenmiyor. Teknik izin hatası artık doğrudan `denied` sayılmıyor; hata mesajı ve tarayıcının gerçek durumu korunuyor. Kayıtlı bildirim tercihi ile cihazın izin durumu arasındaki fark açıklandı. - Sekmeye/pencereye geri dönüldüğünde izin durumu yeniden okunuyor. İzin isteme beklerken ikinci çağrı engelleniyor; kapanmış ekran güncellenmiyor. Teknik izin hatası artık doğrudan `denied` sayılmıyor; hata mesajı ve tarayıcının gerçek durumu korunuyor. Kayıtlı bildirim tercihi ile cihazın izin durumu arasındaki fark açıklandı.
- Doğrulama: son build başarılı. İzole gerçek tarayıcıda beş kontrol geçti: engellenmiş durum, site ayarı açıklaması, tekrar izin düğmesinin yokluğu, yeniden kontrolde durumun korunması ve kayıtlı tercih açıklaması. Kanıt `rapor/qa-notification-permission.json`. Test sekmesi ve bu çalışmanın geçici sunucusu kapatıldı. Bu UI değişikliğinde tam otomatik süit tekrar çalıştırılmadı; önceki süit 262 testte geçmişti. - Doğrulama: son build başarılı. İzole gerçek tarayıcıda beş kontrol geçti: engellenmiş durum, site ayarı açıklaması, tekrar izin düğmesinin yokluğu, yeniden kontrolde durumun korunması ve kayıtlı tercih açıklaması. Kanıt `rapor/qa-notification-permission.json`. Test sekmesi ve bu çalışmanın geçici sunucusu kapatıldı. Bu UI değişikliğinde tam otomatik süit tekrar çalıştırılmadı; önceki süit 262 testte geçmişti.
- Desteklenmeyen/ilk izin/işletim sistemi penceresi ve farklı tarayıcıların izin değişimi olayları ayrıca kabul edilmeli. Gerçek bildirim gönderilmedi; genel plan tamamlanmadı. - Desteklenmeyen/ilk izin/işletim sistemi penceresi ve farklı tarayıcıların izin değişimi olayları ayrıca kabul edilmeli. Gerçek bildirim gönderilmedi; genel plan tamamlanmadı.
### 2026-09-06 — Haftalık tercih iki sekme ve hata kabulü
- Gerçek tarayıcı testinde eski sekmedeki kayıt 409 ile korunuyordu, fakat ekran eski açık/kapalı değerini göstermeye devam ediyor ve genel 'değişiklikleri kopyalayın' mesajı veriyordu. Bu UX hatası düzeltildi: sürüm çakışmasında eski değer ve önizleme kaldırılıyor; açık/kapalı iddiası yerine Özet ayarını yenile düğmesine yönlendiriliyor.
- Tercih yenileme başladığında eski değer temizleniyor; okuma başarısızsa önceki tercih güncelmiş gibi gösterilmiyor. Kayıt hatasında yalnız daha önce onaylanmış değer korunuyor; başarısız seçim başarılı gösterilmiyor.
- İzole QA sunucusuna yalnız test için okuma/kayıt hatası kontrolleri eklendi. Tarayıcıda sekiz kontrol geçti: ilk kapatma, eski yazının reddi, eski değerin gizlenmesi, yenileme yönlendirmesi, okuma hatasında varsayım olmaması, güncel kazanan değerin yüklenmesi, kayıt hatasında onaylı değerin korunması ve başarılı kapatmanın sunucudan tekrar okunması. Kanıt `rapor/qa-weekly-preference-conflict.json`.
- Son build başarılı. Bu UI değişikliğinde tüm otomatik süit tekrar çalıştırılmadı; önceki 262 test sonucu sürüyor. Üç test sekmesi ve bu çalışmanın geçici sunucusu kapatıldı. Gerçek gönderim yapılmadı. Genel planın kalan kabul maddeleri açık.
+6 -2
View File
@@ -11,7 +11,7 @@ function BriefPanel(){
const [value,setValue]=useState(null),[metin,setMetin]=useState(null),[busy,setBusy]=useState(true),[error,setError]=useState(null),[attempt,setAttempt]=useState(0); const [value,setValue]=useState(null),[metin,setMetin]=useState(null),[busy,setBusy]=useState(true),[error,setError]=useState(null),[attempt,setAttempt]=useState(0);
const lifetime=useRef(null),lock=useRef(false); const lifetime=useRef(null),lock=useRef(false);
useEffect(()=>{ useEffect(()=>{
const controller=new AbortController();lifetime.current=controller;lock.current=true;setBusy(true);setError(null); const controller=new AbortController();lifetime.current=controller;lock.current=true;setBusy(true);setError(null);setValue(null);
getWeeklyBrief({signal:controller.signal}).then(data=>{if(!controller.signal.aborted)setValue(data);}) getWeeklyBrief({signal:controller.signal}).then(data=>{if(!controller.signal.aborted)setValue(data);})
.catch(e=>{if(!controller.signal.aborted)setError(e.message);}) .catch(e=>{if(!controller.signal.aborted)setError(e.message);})
.finally(()=>{if(!controller.signal.aborted){lock.current=false;setBusy(false);}}); .finally(()=>{if(!controller.signal.aborted){lock.current=false;setBusy(false);}});
@@ -23,7 +23,11 @@ function BriefPanel(){
try{ try{
const data=preview?await getWeeklyBrief({preview:true,signal}):await saveWeeklyBrief(!value.kapali,value.revision,{signal}); const data=preview?await getWeeklyBrief({preview:true,signal}):await saveWeeklyBrief(!value.kapali,value.revision,{signal});
if(!signal.aborted){setValue(data);if(preview)setMetin(data.metin);} if(!signal.aborted){setValue(data);if(preview)setMetin(data.metin);}
}catch(e){if(!signal.aborted)setError(e.message);} }catch(e){if(!signal.aborted){
if(e.kod==='VERSION_CONFLICT'){
setValue(null);setMetin(null);setError('Bu tercih başka bir sekmede veya cihazda değişti. Güncel durumu görmek için Özet ayarını yenile düğmesine basın.');
}else setError(e.message);
}}
finally{if(!signal.aborted){lock.current=false;setBusy(false);}} finally{if(!signal.aborted){lock.current=false;setBusy(false);}}
}; };
return <section aria-label="Haftalık özet ayarı" className="bg-white rounded-2xl border border-slate-200 p-5 space-y-3"> return <section aria-label="Haftalık özet ayarı" className="bg-white rounded-2xl border border-slate-200 p-5 space-y-3">
+4
View File
@@ -14,6 +14,7 @@ const token=signToken(user);
const keyUser={email:'keys-browser-qa@example.test',name:'API QA',isAdmin:true}; const keyUser={email:'keys-browser-qa@example.test',name:'API QA',isAdmin:true};
const keyToken=signToken(keyUser); const keyToken=signToken(keyUser);
let apiKeyListFailure=false; let apiKeyListFailure=false;
let briefFailure='none';
process.env.IHALEIST_USER_DATA_DIR=dir; process.env.IHALEIST_USER_DATA_DIR=dir;
const {createBriefOutbox}=await import('../server/brief-outbox.mjs'); const {createBriefOutbox}=await import('../server/brief-outbox.mjs');
const qaBriefOutbox=createBriefOutbox(dir); const qaBriefOutbox=createBriefOutbox(dir);
@@ -72,6 +73,9 @@ const ui=await createServer({server:{host:'127.0.0.1',port:0,proxy:{'/api':{targ
res.setHeader('Content-Type','application/json');res.end(JSON.stringify({success:true,job:qaJobs.getAiJob(user.email,created.id)})); res.setHeader('Content-Type','application/json');res.end(JSON.stringify({success:true,job:qaJobs.getAiJob(user.email,created.id)}));
}catch(error){res.writeHead(400,{'Content-Type':'application/json'});res.end(JSON.stringify({success:false,error:error.message}));}});return; }catch(error){res.writeHead(400,{'Content-Type':'application/json'});res.end(JSON.stringify({success:false,error:error.message}));}});return;
} }
if(req.url==='/qa-brief-controls') {res.setHeader('Content-Type','text/html; charset=utf-8');res.end(`<html lang="tr"><h1>Yerel haftalık özet testi</h1><button id="read">Özet okuma hatası</button><button id="write">Özet kayıt hatası</button><button id="none">Özet hatalarını kapat</button><p role="status"></p><script>for(const mode of ['read','write','none'])document.getElementById(mode).onclick=async()=>{const r=await fetch('/qa-brief-failure?mode='+mode,{method:'POST'});document.querySelector('[role=status]').textContent=await r.text();};</script></html>`);return;}
if(req.url?.startsWith('/qa-brief-failure?')&&req.method==='POST'){briefFailure=new URL(req.url,'http://localhost').searchParams.get('mode');res.end('Özet test durumu: '+briefFailure);return;}
if(req.url?.startsWith('/api/me/brief')&&((briefFailure==='read'&&req.method==='GET')||(briefFailure==='write'&&req.method==='POST'))){res.writeHead(503,{'Content-Type':'application/json'});res.end(JSON.stringify({success:false,error:'QA: Haftalık özet deposuna erişilemiyor.'}));return;}
if(req.url==='/qa-api-controls') {res.setHeader('Content-Type','text/html; charset=utf-8');res.end(`<html lang="tr"><h1>Yerel API testi</h1><p>Yalnız geçici test deposunu etkiler.</p><button id="login">API test hesabıyla gir</button><button id="fail">Liste hatasını aç</button><button id="recover">Liste hatasını kapat</button><p role="status"></p><script type="module">import {setSession} from '/src/services/auth.js';document.querySelector('#login').onclick=()=>{setSession(${JSON.stringify(keyToken)},${JSON.stringify(keyUser)});location.href='/';};for(const [id,mode] of [['fail','on'],['recover','off']])document.getElementById(id).onclick=async()=>{await fetch('/qa-api-failure?mode='+mode,{method:'POST'});document.querySelector('[role=status]').textContent=mode==='on'?'Liste hatası açık':'Liste hatası kapalı';};</script></html>`);return;} if(req.url==='/qa-api-controls') {res.setHeader('Content-Type','text/html; charset=utf-8');res.end(`<html lang="tr"><h1>Yerel API testi</h1><p>Yalnız geçici test deposunu etkiler.</p><button id="login">API test hesabıyla gir</button><button id="fail">Liste hatasını aç</button><button id="recover">Liste hatasını kapat</button><p role="status"></p><script type="module">import {setSession} from '/src/services/auth.js';document.querySelector('#login').onclick=()=>{setSession(${JSON.stringify(keyToken)},${JSON.stringify(keyUser)});location.href='/';};for(const [id,mode] of [['fail','on'],['recover','off']])document.getElementById(id).onclick=async()=>{await fetch('/qa-api-failure?mode='+mode,{method:'POST'});document.querySelector('[role=status]').textContent=mode==='on'?'Liste hatası açık':'Liste hatası kapalı';};</script></html>`);return;}
if(req.method==='POST' && req.url?.startsWith('/qa-api-failure?')) {apiKeyListFailure=new URL(req.url,'http://localhost').searchParams.get('mode')==='on';res.end('ok');return;} if(req.method==='POST' && req.url?.startsWith('/qa-api-failure?')) {apiKeyListFailure=new URL(req.url,'http://localhost').searchParams.get('mode')==='on';res.end('ok');return;}
if(apiKeyListFailure && req.method==='GET' && req.url==='/api/me/apikeys') {res.writeHead(503,{'Content-Type':'application/json'});res.end(JSON.stringify({success:false,error:'QA: Anahtar deposuna erişilemiyor.'}));return;} if(apiKeyListFailure && req.method==='GET' && req.url==='/api/me/apikeys') {res.writeHead(503,{'Content-Type':'application/json'});res.end(JSON.stringify({success:false,error:'QA: Anahtar deposuna erişilemiyor.'}));return;}