diff --git a/docs/quality/uygulama-durumu.md b/docs/quality/uygulama-durumu.md index fc1b33f..f120591 100644 --- a/docs/quality/uygulama-durumu.md +++ b/docs/quality/uygulama-durumu.md @@ -1040,3 +1040,10 @@ Taslak türü seçicisine erişilebilir ad eklendi. Üretim derlemesi geçti. Ö - İzole tarayıcı testinde push kapatma ve sessiz saat açma kalıcıydı, fakat 21:30 yazılan başlangıç yenilemede 22:00 dönüyordu. time input olayını da dinleyerek görünür değerin kayda aktarılmaması düzeltildi; bitiş alanına aynı düzeltme uygulandı. - Yenileme sonrası 21:30 başlangıç, 07:45 bitiş, push kapalı ve sessiz saat açık durumları doğrulandı. E-posta yapılandırılmadı durumu, gönderim düğmesinin devre dışı olması ve işlevsiz genel e-posta kutusunun yokluğu da doğrulandı. Yedi kontrol geçti; kanıt qa-reminder-preferences.json önce/sonra görünümü içerir. - Derleme geçti. İş mantığı değişmediğinden önceki 235 test tekrarlanmadı; bu değişiklik doğrudan tarayıcıda doğrulandı. İzin penceresi açılmadı ve gerçek gönderim yapılmadı. E-posta config ağ hatası yeniden deneme ve iki sekme tercih çatışması tarayıcı kabulü açık. Test sekmesi ve yeni QA süreci exit 0 ile kapatıldı. + +### Haftalık özet tercihinin gerçek kaydı — 6 Eylül 2026 +- API testi haftalık özet kaydının 400 döndüğünü ortaya çıkardı: brief anahtarı user-store izinli anahtarlar listesinde yoktu. Hem yazım hem getData okuması için izinli kayda eklendi; mevcut arayüz başarısız HTTP yanıtını kontrol etmediğinden anahtarı değişmiş gibi gösteriyordu. +- Haftalık özet paneli sunucu onayını bekler; ilk yüklemede açık varsayımı göstermez. Süre/oturum/iptal kontrollü istek, tek işlem kilidi, erişilebilir switch etiketi, hata ve ayar yenileme düğmesi eklendi. Eski sekmeden kayıt içerik sürümüyle reddedilir. Geçersiz POST gövdesi özeti yanlışlıkla açamaz. +- Tercih GET isteği artık ihale havuzunun yüklenmesine bağlı değildir; içerik yalnız preview=1 ile Önizle işleminde oluşturulur. Genel kullanıcı veri rotasında brief yazımı da sürüm/boolean kontrolünden geçer. Gönderim metni hizmet ve zamanlayıcı etkinliğine bağlı olduğunu açıklar. +- 236 otomatik test ve derleme geçti. HTTP testi başlangıç tercihi, gövde doğrulama, kapatma/geri okuma, eski sürüm reddi, başka hesabın ayrımı ve güncel sürümle açmayı doğruladı. İlk testteki 400 bu gerçek izinli-anahtar hatasını yakaladı; düzeltme sonrası tüm suite geçti. +- Yeni panelin tarayıcı hata/kurtarma ve önizleme kabulü, zamanlayıcıdan gerçek test teslimi açık. Bu tur dış özet gönderilmedi; canlıya yayın yapılmadı. diff --git a/docs/quality/yedek-geri-yukleme.md b/docs/quality/yedek-geri-yukleme.md index bc22f46..cfb5aca 100644 --- a/docs/quality/yedek-geri-yukleme.md +++ b/docs/quality/yedek-geri-yukleme.md @@ -83,3 +83,5 @@ Favori kurumlar (`favori_kurumlar`) da sürümlü kullanıcı verisi yazımı is Rakip izleme (`rakip_izleme`) da içerik sürümü zorunlu anahtarlara dahil edildi. Eski istemcilerde sürümsüz yazım 428 alır; güncel frontend ile aynı sürüm backend kullanılmalıdır. Eski rakip kayıtlarındaki ölçüler yeni arayüzde güncel havuz eşleşmesi yerine kullanılmaz; kayıtlı firma adı/sektör korunur. Bildirim tercihleri (`reminder`) sürümlü yazım sözleşmesine dahil edildi. Saat alanları HH:mm, pushEnabled/quietEnabled alanları boolean olmalıdır. Frontend/backend birlikte güncellenmelidir. Eski e-postaEnabled kaydı e-posta aboneliği iptali anlamına gelmez; gönderimler abonelik yönetiminden denetlenir. + +Haftalık özet `brief` anahtarı user-store tarafından artık kabul edilir. `/api/me/brief` GET tercih ve revision döndürür; metin/brief önizlemesi için `?preview=1` gerekir. POST boolean kapali ve okunan revision ister. Frontend/backend birlikte güncellenmelidir; eski sürüme dönmek brief kaydını tekrar reddeder ve opt-out tercihini okuyamayabilir. diff --git a/server/index.js b/server/index.js index 17cb0d0..5bcfe77 100644 --- a/server/index.js +++ b/server/index.js @@ -1213,7 +1213,8 @@ const server = http.createServer(async (req, res) => { if(k==='rakip_izleme' && (!Array.isArray(body.deger) || body.deger.some(item=>!item || typeof item.id!=='string'||!item.id||typeof item.name!=='string'||!item.name.trim()||item.name.length>1000||(item.sector!=null&&typeof item.sector!=='string')) || new Set(body.deger.map(item=>item.id)).size!==body.deger.length || new Set(body.deger.map(item=>item.name.trim().toLocaleLowerCase('tr-TR'))).size!==body.deger.length)) return json(res,400,{success:false,error:'Rakip listesi geçersiz veya yinelenen firma içeriyor.'}); if(k==='reminder')validateReminderPreferences(body.deger); - const protectedKey=['firma_profil','saved_filters','ekip','favori_kurumlar','rakip_izleme','reminder'].includes(k); + if(k==='brief'&&typeof body.deger?.kapali!=='boolean')return json(res,400,{success:false,error:'Haftalık özet seçimi geçersiz.'}); + const protectedKey=['firma_profil','saved_filters','ekip','favori_kurumlar','rakip_izleme','reminder','brief'].includes(k); if(protectedKey && (!Object.hasOwn(body,'revision') || (body.revision!==null && (typeof body.revision!=='string' || !/^[a-f0-9]{64}$/.test(body.revision))))) return json(res,428,{success:false,kod:'VERSION_REQUIRED',error:'Güncel kayıt sürümü gerekli. Sayfayı yenileyip tekrar deneyin.'}); const revision=store.setData(user.email, k, body.deger, protectedKey ? body.revision : undefined); @@ -1514,15 +1515,25 @@ const server = http.createServer(async (req, res) => { const user = userFromAuthHeader(req.headers['authorization']); if (!user) return json(res, 401, { success: false, error: 'oturum yok' }); store.ensureUser(user.email, user.name); - if (req.method === 'POST') { - const body = await readBody(req).catch(() => ({})); - store.setData(user.email, 'brief', { kapali: body.kapali === true }); - return json(res, 200, { success: true, kapali: body.kapali === true }); - } - const out = await tendersPayload(); - const b = briefOlustur(user.email, out.tenders); - return json(res, 200, { success: true, kapali: !!(store.getData(user.email, 'brief')?.kapali), brief: b, metin: briefMetni(b) }); + try { + if(req.method==='POST'){ + const body=await readBody(req); + if(typeof body.kapali!=='boolean')return json(res,400,{success:false,error:'Haftalık özet seçimi geçersiz.'}); + if(!Object.hasOwn(body,'revision')||(body.revision!==null&&(typeof body.revision!=='string'||!/^[a-f0-9]{64}$/.test(body.revision))))return json(res,428,{success:false,error:'Güncel özet ayarını yükleyin.'}); + const revision=store.setData(user.email,'brief',{kapali:body.kapali},body.revision); + return json(res,200,{success:true,kapali:body.kapali,revision}); + } + if(req.method!=='GET')return json(res,405,{success:false,error:'Desteklenmeyen metot.'}); + let preview={}; + if(url.searchParams.get('preview')==='1'){ + const out=await tendersPayload(),b=briefOlustur(user.email,out.tenders); + preview={brief:b,metin:briefMetni(b)}; + } + const snapshot=store.getDataSnapshot(user.email); + return json(res,200,{success:true,kapali:!!snapshot.veri.brief?.kapali,revision:snapshot.revisions.brief ?? null,...preview}); + }catch(e){return json(res,e.kod==='VERSION_CONFLICT'?409:400,{success:false,error:e.message,kod:e.kod});} } + if (url.pathname === '/api/admin/brief/gonder' && req.method === 'POST') { const user = userFromAuthHeader(req.headers['authorization']); if (!user?.isAdmin) return json(res, 403, { success: false, error: 'yetki yok' }); diff --git a/server/user-store.mjs b/server/user-store.mjs index 6f94fda..89114f2 100644 --- a/server/user-store.mjs +++ b/server/user-store.mjs @@ -184,7 +184,7 @@ export function findByStripe({ customerId, subId }) { // --- Kullanıcı verisi (localStorage yerine) -------------------------------- const IZINLI_ANAHTARLAR = new Set([ - 'favori_kurumlar', 'ekip', 'saved_filters', 'reminder', + 'favori_kurumlar', 'ekip', 'saved_filters', 'reminder', 'brief', 'rakip_izleme', 'ciro_hedefi', 'ai_model_config', 'kanban', 'iptal_geri_bildirim', 'firma_profil', // yetkinlikler + uyum eşiği diff --git a/src/components/tools/BriefTercihi.jsx b/src/components/tools/BriefTercihi.jsx index 0122221..8992f0c 100644 --- a/src/components/tools/BriefTercihi.jsx +++ b/src/components/tools/BriefTercihi.jsx @@ -1,48 +1,41 @@ -import React, { useEffect, useState } from 'react'; -import { CalendarClock, Eye, Loader2 } from 'lucide-react'; -import { getToken } from '../../services/auth'; - -/** - * Haftalık brief tercihi (Pazartesi 08:00): bu hafta yapılacak takipli - * ihaleler + geçen hafta sonuçlanan takipliler — Telegram ve e-posta. - * "Önizle" şu anki içeriği gösterir; anahtar kapatır. - */ -const iste = (opts = {}) => fetch('/api/me/brief', { headers: { Authorization: `Bearer ${getToken()}`, 'Content-Type': 'application/json' }, ...opts }).then((r) => r.json()); - -export const BriefTercihi = () => { - const [kapali, setKapali] = useState(false); - const [metin, setMetin] = useState(null); - const [mesgul, setMesgul] = useState(false); - - useEffect(() => { iste().then((d) => { if (d?.success) setKapali(!!d.kapali); }).catch(() => {}); }, []); - - const degistir = async () => { - const yeni = !kapali; setKapali(yeni); - try { await iste({ method: 'POST', body: JSON.stringify({ kapali: yeni }) }); } catch { setKapali(!yeni); } - }; - const onizle = async () => { - setMesgul(true); - try { const d = await iste(); setMetin(d?.metin || 'Önizleme alınamadı.'); } catch { setMetin('Önizleme alınamadı.'); } finally { setMesgul(false); } - }; - - return ( -
Her Pazartesi 08:00: bu hafta yapılacak takipli ihaleler ve geçen hafta sonuçlanan takipliler — Telegram (bağlıysa) ve e-posta ile tek mesaj.
-{metin}}
- Haftanın takipli ihaleleri ve geçen hafta sonuçlanan takipliler için özet tercihi. Gönderim, sunucu zamanlayıcısının ve ilgili e-posta/Telegram hizmetinin etkin olmasına bağlıdır.
+Haftalık özet işlemi sürüyor…
} + {error&&{error}
} + {metin!==null&&{metin}}
+