diff --git a/docs/quality/uygulama-durumu.md b/docs/quality/uygulama-durumu.md
index a116a96..d97f351 100644
--- a/docs/quality/uygulama-durumu.md
+++ b/docs/quality/uygulama-durumu.md
@@ -1023,3 +1023,9 @@ Taslak türü seçicisine erişilebilir ad eklendi. Üretim derlemesi geçti. Ö
- Sunucuda rakip özel bildirim tüketicisi bulunmadığı doğrulandı. Otomatik bildirimler aktif metni, proaktif uyarı vaadi ve navigasyondaki Canlı Uyar rozeti kaldırıldı. Bildirim kontrolleri etkin değil olarak gösterilir; dış mesaj gönderilmez. Yasaklı sorgu bağlantısı da doğrulanmamış pano-kopyalandı iddiası taşımaz.
- 230 otomatik test ve derleme geçti. Rakip API sürüm zorunluluğu, eski sürüm, bozuk/yinelenen liste, kayıt korunması ve kaldırma test edildi. İzole tarayıcıda 7 kontrol geçti: kayıt, üç ölçüde Veri yok, bildirimlerin pasif olması, yenileme kalıcılığı, yanıltıcı rozetin kalkması, yinelenen ekleme engeli, çıkarma. Kanıt qa-radar-persistence.json.
- Gerçek kazanan kaynaklarıyla eşleşmenin ekonomik doğruluğu, rakip özel bildirim özelliğinin uygulanması ve iki sekmeli rakip UI çakışması bu kabulde tamamlanmadı. Bu değişiklik çalışmayan bildirim özelliğini çalışıyor göstermez. QA sekmesi ve yeni süreç kapatıldı; canlıya yayın yapılmadı.
+
+### Tarayıcı bildirimi ve sessiz saat kayıtları — 6 Eylül 2026
+- reminder ayarı iyimser otomatik kayıttan sürümlü, sunucu onaylı kayda geçirildi. Beklerken ayar kontrolleri kilitlenir; hata görünür, etkin ayar başarısız yazımla değişmez. Sunucu boolean seçimleri ve HH:mm saatlerini doğrular; eski sekme güncel ayarı ezmez.
+- Sessiz saat hesabı ortak fonksiyona taşındı. Gece yarısı, başlangıç dahil/bitiş hariç ve eşit başlangıç-bitiş davranışı doğrulandı. Saatlerin cihazın yerel saatine göre olduğu açıkça yazıldı. Geciken bildirim callback'leri güncel ayar ref'ini okur; eski sessiz saat/pushEnabled closure'ı kullanılmaz.
+- Gerçek e-posta gönderiminde kullanılmayan E-posta bildirimleri açık kutusu kaldırıldı; kullanıcı gerçek bildirim aboneliklerine yönlendirilir. Tarayıcı/sessiz saat seçiminin e-posta aboneliğini kapatmadığı belirtilir. Tarayıcı ayarında açık uygulama bildirimi ve kapalı uygulamaya arka plan push teslimi ayrımı yapıldı; bir gösterim denemesi kesin teslim olarak sunulmaz.
+- 233 otomatik test ve derleme geçti. Saat sınırları, geçersiz ayarlar, eski sürümle yazım ve mevcut ayarın korunması test edildi. Gerçek tarayıcı izin penceresi, yeni ayarların tarayıcı kalıcılığı ve iki sekmeli UI çatışması açık. E-posta yapılandırma/test gönderimi HTTP hata davranışı ayrıca incelenmeli. Dış e-posta veya bildirim 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 c901169..bc22f46 100644
--- a/docs/quality/yedek-geri-yukleme.md
+++ b/docs/quality/yedek-geri-yukleme.md
@@ -81,3 +81,5 @@ Ekip kaydı (`ekip`) da aynı içerik sürümü sözleşmesine dahil edildi. Yen
Favori kurumlar (`favori_kurumlar`) da sürümlü kullanıcı verisi yazımı ister. Eski istemcinin sürümsüz yazımı 428 döner; frontend/backend birlikte sürümlenmelidir. Bekleyen değişiklikler sunucudan onay gelmeden favori listesinde gösterilmez.
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.
diff --git a/server/index.js b/server/index.js
index 96273eb..f63f7ab 100644
--- a/server/index.js
+++ b/server/index.js
@@ -1,3 +1,4 @@
+import {validateReminderPreferences} from '../src/services/reminderPreferences.js';
import {validateTeam} from '../src/services/teamData.js';
import { executeAnalysisJob } from './analysis-job.mjs';
import { resolveAiJobInput } from './ai-job-source.mjs';
@@ -1211,7 +1212,8 @@ const server = http.createServer(async (req, res) => {
return json(res,400,{success:false,error:'Favori kurum listesi geçersiz veya yinelenen kurum içeriyor.'});
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.'});
- const protectedKey=['firma_profil','saved_filters','ekip','favori_kurumlar','rakip_izleme'].includes(k);
+ if(k==='reminder')validateReminderPreferences(body.deger);
+ const protectedKey=['firma_profil','saved_filters','ekip','favori_kurumlar','rakip_izleme','reminder'].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);
diff --git a/src/components/tools/PwaSettings.jsx b/src/components/tools/PwaSettings.jsx
index 9ba73e2..e8bc6a1 100644
--- a/src/components/tools/PwaSettings.jsx
+++ b/src/components/tools/PwaSettings.jsx
@@ -2,12 +2,11 @@ import {DeliveryQueue} from './DeliveryQueue';
import React, { useState, useEffect } from 'react';
import { useApp } from '../../context/AppContext';
import { Smartphone, Wifi, WifiOff, Download, Bell, CheckCircle2, Moon, Mail, Send } from 'lucide-react';
-import confetti from 'canvas-confetti';
import { getToken } from '../../services/auth';
import { NotifySubscriptions } from './NotifySubscriptions';
export const PwaSettings = () => {
- const { userProfile, authUser, tenders, reminderSettings, setReminderSettings, requestPushPermission, notify, isQuietNow } = useApp();
+ const { userProfile, authUser, tenders, reminderSettings, saveReminderSettings, reminderSaving, reminderError, requestPushPermission, notify, isQuietNow } = useApp();
// Gerçek Cache API sayımı — sahte "102 ihale önbellekte" yerine
const [onbellekSayisi, setOnbellekSayisi] = useState(null);
const [onbellekAdi, setOnbellekAdi] = useState('');
@@ -73,12 +72,10 @@ export const PwaSettings = () => {
const p = await requestPushPermission();
setPermission(p);
if (p === 'granted') {
- setReminderSettings((s) => ({ ...s, pushEnabled: true }));
- notify('Bildirimler açıldı', 'Yeni ihale ve son tarih uyarıları buradan gelecek.');
- confetti({ particleCount: 50 });
+ await saveReminderSettings((s) => ({ ...s, pushEnabled: true }));
}
};
- const setQuiet = (patch) => setReminderSettings((s) => ({ ...s, ...patch }));
+ const setQuiet = (patch) => saveReminderSettings((s) => ({ ...s, ...patch }));
// Gerçek PWA kurulumu: tarayıcı beforeinstallprompt verirse yerel diyalog;
// zaten standalone açıldıysa "yüklü"; iOS Safari'de olay yok → yol tarifi.
@@ -173,36 +170,42 @@ export const PwaSettings = () => {
- Tarayıcı bildirimlerine izin verin; yeni ihale ve son tarih uyarıları gerçek bildirim olarak gelir.
+ Bu uygulama açıkken alınan uyarıları tarayıcı bildirimi olarak gösterebilirsiniz. Kapalı uygulamaya arka plan push teslimi sağlanmaz.