From 2353170c408d9514296a4eaea0e370941e75e191 Mon Sep 17 00:00:00 2001 From: ht Date: Sun, 6 Sep 2026 14:56:21 +0300 Subject: [PATCH] fix: retain server notifications when local alerts arrive --- docs/quality/uygulama-durumu.md | 7 +++++++ src/context/AppContext.jsx | 6 +++--- src/services/notifications.js | 9 ++++++++- tests/notifications-client.test.mjs | 19 ++++++++++++++++++- 4 files changed, 36 insertions(+), 5 deletions(-) diff --git a/docs/quality/uygulama-durumu.md b/docs/quality/uygulama-durumu.md index e01a768..6659744 100644 --- a/docs/quality/uygulama-durumu.md +++ b/docs/quality/uygulama-durumu.md @@ -1138,3 +1138,10 @@ Taslak türü seçicisine erişilebilir ad eklendi. Üretim derlemesi geçti. Ö - getUpdates doğrulaması HTTP başarısı, boolean ok:true ve gerçek sonuç dizisi gerektiriyor. Eksik/yanlış tipte sonuçlar artık boş liste gibi 'kod bulunamadı' sayılmıyor; sağlayıcı yanıt hatası olarak bildiriliyor. - 269 test ve build başarılı. Yeni testler kaydedilmiş bağlantı + yapılandırılmamış hizmetin HTTP 503 vermesi, kimliksiz test kabulünün istemcide reddi/tekrarlanmaması ve bozuk güncelleme yanıtlarını kapsıyor. HTTP test sunucusunda Telegram tokenı açıkça boş bırakılıyor; yalnız geçici veri deposu kullanılıyor. Gerçek Telegram mesajı gönderilmedi. - Test gönderimi yanıtında id alanı zorunlu hale geldi; ön yüz/sunucu birlikte güncellenmeli. Test mesajı düğmesi için kalıcı istek tekrar engeli ve gerçek sağlayıcı uçtan uca kabulü henüz doğrulanmadı. Genel plan açık. + +### 2026-09-06 — Yerel uyarılar sunucu bildirimlerini düşürmesin + +- Yeni bir yerel uyarı ekleyen `notify` işlevi tüm listeyi ilk 30 kayıtla sınırlıyordu. Bu, sunucudan yüklenen ve henüz okunmamış ihale bildirimlerini de ekrandan düşürüyordu. Yeni ortak birleştirme işlevi yalnız son 30 yerel uyarıyı tutuyor; yüklenmiş sunucu kayıtları sayı sınırına dahil edilmiyor. +- Sunucu yenilemesindeki toplam 80 kayıt kesintisi de kaldırıldı; yenileme sunucunun gönderdiği kayıtları/okunma durumunu esas alıp yerel uyarıları kendi sınırı içinde koruyor. Yerel bildirim kimlikleri aynı milisaniyede çakışabilen Date.now yerine UUID kullanıyor. +- Doğrulama: 270 test ve build başarılı. Yeni regresyon testinde 100 sunucu kaydı üzerine 40 yerel uyarı eklendi; tüm sunucu kayıtları ve okunma değerleri korundu, son 30 yerel uyarı tekil kimlikle kaldı, sunucudan yeni okunma durumu geldiğinde yerel kayıtlar korundu. Bu sayı testi istemcinin aldığı listeyi kapsar; sunucu bildirim geçmişinin bütününü/paginasyonunu kanıtlamaz. +- Bu değişiklik için gerçek tarayıcıdaki yoğun bildirim görsel kabulü henüz yapılmadı. Gerçek bildirim gönderilmedi, canlıya yayın yapılmadı. Genel plan açık. diff --git a/src/context/AppContext.jsx b/src/context/AppContext.jsx index 9996fc6..3421cba 100644 --- a/src/context/AppContext.jsx +++ b/src/context/AppContext.jsx @@ -1,7 +1,7 @@ import {validateReminderPreferences,quietNow} from '../services/reminderPreferences'; import {validateTeam} from '../services/teamData'; import {savedFilterCriteria} from '../services/savedFilterCriteria'; -import {mergeServerNotifications,acknowledgeNotifications} from '../services/notifications'; +import {mergeServerNotifications,acknowledgeNotifications,addLocalNotification} from '../services/notifications'; import { searchTender } from '../services/searchTender'; import { normalizeTenderSearch, matchingItemResults } from '../services/tenderSearch'; import React, {createContext, useContext, useState, useEffect, useMemo, useRef } from 'react'; @@ -579,7 +579,7 @@ export const AppProvider = ({ children }) => { // Gerçek bildirim: listeye ekler; sessiz saat dışında ve push açıksa tarayıcı // bildirimi (Notification API) da gösterir. const notify = (title, text) => { - setNotifications((prev) => [{ id: 'n-' + Date.now(), title, text, time: 'az önce', read: false }, ...prev].slice(0, 30)); + setNotifications(prev=>addLocalNotification(prev,title,text)); pushNotification(title,text); }; const pushNotification=(title,text)=>{ @@ -605,7 +605,7 @@ export const AppProvider = ({ children }) => { const fresh=checked.filter(row=>!row.read&&!seenServerNotifications.current.has(row.id)); checked.forEach(row=>seenServerNotifications.current.add(row.id)); fresh.slice(0,5).forEach(row=>pushNotification(row.title,row.text)); - setNotifications(current=>[...checked,...current.filter(row=>!row.serverId)].slice(0,80)); + setNotifications(current=>mergeServerNotifications(current,rows)); }catch(error){if(getToken()===session&&version===notificationVersion.current)setNotificationError(error.message);} }; const markNotificationsRead=async()=>{ diff --git a/src/services/notifications.js b/src/services/notifications.js index 5bc2d05..77fa348 100644 --- a/src/services/notifications.js +++ b/src/services/notifications.js @@ -1,8 +1,15 @@ +const keepRecentLocal = rows => { + let localCount=0; + return rows.filter(row=>row.serverId || ++localCount<=30); +}; +export function addLocalNotification(current,title,text) { + return keepRecentLocal([{id:'n-'+crypto.randomUUID(),title,text,time:'az önce',read:false},...current]); +} export function mergeServerNotifications(current,rows) { if(!Array.isArray(rows)||rows.some(row=>typeof row?.id!=='string'||typeof row.baslik!=='string'||typeof row.metin!=='string'))throw new Error('Bildirim listesi geçersiz.'); const remote=rows.map(row=>({id:'server:'+row.id,serverId:row.id,title:row.baslik,text:row.metin, time:new Date(row.zaman).toLocaleString('tr-TR'),read:!!row.okundu,ikn:row.ikn})); - return [...remote,...current.filter(row=>!row.serverId)].slice(0,80); + return keepRecentLocal([...remote,...current.filter(row=>!row.serverId)]); } export async function acknowledgeNotifications(snapshot,request) { const unread=snapshot.filter(row=>!row.read),ids=unread.filter(row=>row.serverId).map(row=>row.serverId); diff --git a/tests/notifications-client.test.mjs b/tests/notifications-client.test.mjs index 9c2144d..24122c8 100644 --- a/tests/notifications-client.test.mjs +++ b/tests/notifications-client.test.mjs @@ -1,6 +1,6 @@ import test from 'node:test'; import assert from 'node:assert/strict'; -import {mergeServerNotifications,acknowledgeNotifications} from '../src/services/notifications.js'; +import {mergeServerNotifications,acknowledgeNotifications,addLocalNotification} from '../src/services/notifications.js'; const rows=[{id:'one',baslik:'Tarih değişti',metin:'Yeni tarih',okundu:0,zaman:'2026-09-06T10:00:00Z'}]; test('refresh keeps stable notification identities and server unread state without acknowledging delivery',()=>{ const once=mergeServerNotifications([{id:'local',title:'Yerel',read:false}],rows); @@ -17,3 +17,20 @@ test('read acknowledgement requires server success and only covers the captured const acknowledged=await acknowledgeNotifications(current,async ids=>{assert.deepEqual(ids,['one']);return {success:true,okundu:1};}); assert.deepEqual([...acknowledged],['server:one','local']);assert.equal(acknowledged.has('arrived-later'),false); }); + + +test('local notification bursts preserve every loaded server record and its read state',()=>{ + const remote=Array.from({length:100},(_,i)=>({id:String(i),baslik:'İhale '+i,metin:'Kaynak bildirimi',okundu:i%2,zaman:'2026-09-06T10:00:00Z'})); + let list=mergeServerNotifications([],remote); + const before=list.map(row=>({...row})); + for(let i=0;i<40;i++)list=addLocalNotification(list,'Yerel '+i,'İşlem sonucu'); + assert.deepEqual(list.filter(row=>row.serverId),before); + const local=list.filter(row=>!row.serverId); + assert.equal(local.length,30); + assert.equal(local[0].title,'Yerel 39');assert.equal(local.at(-1).title,'Yerel 10'); + assert.equal(new Set(local.map(row=>row.id)).size,30); + const refreshed=mergeServerNotifications(list,remote.map(row=>({...row,okundu:1}))); + assert.equal(refreshed.filter(row=>row.serverId).length,100); + assert.ok(refreshed.filter(row=>row.serverId).every(row=>row.read)); + assert.deepEqual(refreshed.filter(row=>!row.serverId),local); +});