From c7bb67b101a439fbccc8bd452a53d64a4496323d Mon Sep 17 00:00:00 2001 From: ht Date: Sun, 6 Sep 2026 12:50:06 +0300 Subject: [PATCH] Correct calendar deadline export and isolate notification storage --- docs/quality/uygulama-durumu.md | 7 +++++++ docs/quality/yedek-geri-yukleme.md | 2 ++ server/zeyilname.mjs | 5 +++-- src/components/TenderDetailModal.jsx | 3 ++- src/services/tenderCalendar.js | 6 +++--- tests/calendar.test.mjs | 13 +++++++++++++ tests/notification-store.test.mjs | 24 ++++++++++++++++++++++++ 7 files changed, 54 insertions(+), 6 deletions(-) create mode 100644 tests/notification-store.test.mjs diff --git a/docs/quality/uygulama-durumu.md b/docs/quality/uygulama-durumu.md index deabf0b..d1ab701 100644 --- a/docs/quality/uygulama-durumu.md +++ b/docs/quality/uygulama-durumu.md @@ -760,3 +760,10 @@ Taslak türü seçicisine erişilebilir ad eklendi. Üretim derlemesi geçti. Ö - Lease dolmadan çağrı yapılmaması, lease dolunca interrupted durumu, açık retry olmadan çağrı yapılmaması, retry sonrasında aynı kaynak/sonuç ve başka hesaba erişim engeli doğrulandı. Uzak kaynak B çözücüsü hiç çağrılmadı; sabit kaynak A kullanıldı. Her iki SQLite integrity_check sonucu ok. - 169 otomatik test geçti. Üretim kodu değişmediği için önceki başarılı derleme tekrar edilmedi. - Test lease değerlendirme saatini ilerletir; gerçek 60 saniyelik bekleme, gerçek sağlayıcı isteği, elektrik kesintisi veya platform deploy/volume kurtarma kabulü değildir. Tam D11 yayın/geri dönüş provası açık. + +### Takvim dosyası ve bildirim deposu — 6 Eylül 2026 +- ICS çıktısındaki DTSTART ile aynı ana yazılan DTEND kaldırıldı. Son teklif anı ek süre uydurmadan, TRANSP:TRANSPARENT olarak dışa aktarılır. RFC 5545 §3.6.1 ve §3.8.2.2: https://www.rfc-editor.org/rfc/rfc5545.html +- İhale tarihi değiştiğinde UID sabit kalır; dosya ve arayüz bunun otomatik takvim aboneliği olmadığını açıklar. Düğme “Takvim Dosyasını İndir (.ics)” olarak adlandırıldı. Tek başına UID, Google/Outlook dosya içe aktarımında mevcut kaydın güncelleneceğini garanti etmez. +- Tek başına CR karakteri de ICS metninde kaçırılıyor; ek VEVENT enjeksiyonu testi eklendi. +- Bildirim deposunun sabit server/data yolu, IHALEIST_USER_DATA_DIR ayarına bağlandı; varsayılan yol korunur. Yeni dosya izinleri 0600. İki ayrı süreçte kalıcılık ve hesap bazlı okundu işaretleme test edildi. Bu değişiklik test deposunun üretim bildirimleriyle karışmasını ve özel volume yedeğinin bildirimleri atlamasını önler. +- 171 test ve üretim derlemesi geçti. Özel veri dizini kullanan eski kurulumlar için takip.sqlite taşıma gereksinimi runbook'a eklendi. Üretim taşıması yapılmadı; gerçek Google/Outlook içe aktarımı ve zeyilname bildiriminin dış kanallara teslimi henüz kabul edilmedi. diff --git a/docs/quality/yedek-geri-yukleme.md b/docs/quality/yedek-geri-yukleme.md index 8f7d2cf..78e9850 100644 --- a/docs/quality/yedek-geri-yukleme.md +++ b/docs/quality/yedek-geri-yukleme.md @@ -59,3 +59,5 @@ Kalıcı genel analiz ve teklif işleri için süreç kesintisi davranışı: - İnsan incelemeli genel analizde dosya/inceleme sürümü değişmişse eski işi yeniden başlatmak yerine güncel incelemeyle yeni iş gerekir. - Kaynağın sabitlenmesinden önce kesilen işin, geçmişte hangi uzak metni edinmiş olabileceği kanıtlanamaz. Eski sürümde tamamlanan kayıtların kaynakları geriye dönük üretilemez. - `node --test tests/ai-job-crash.test.mjs`: iki gerçek alt süreç SIGKILL testi; yeni süreçte kaynak/sonuç ve SQLite bütünlüğü kontrol edilir. Lease süresi testte saat parametresiyle ilerletilir. Gerçek AI sağlayıcısı ve üretim platformunun volume yeniden bağlama davranışı bu testin kapsamı dışındadır. + +Bildirim ve takip taraması deposu artık `IHALEIST_USER_DATA_DIR/takip.sqlite` içindedir; değişken yoksa mevcut `server/data/takip.sqlite` yolu korunur. Özel kullanıcı veri diziniyle çalışan eski kurulum yükseltilirken eski sabit yoldaki takip.sqlite ayrıca envantere alınmalıdır. Tüm yazıcılar durdurularak SQLite tutarlı yedeği doğrulanmalı ve hedefte mevcut bir takip.sqlite üzerine yazmadan yeni dizine aktarılmalıdır. Bu yerel değişiklik üretim verisini taşımadı. Tam volume yedeği bu dosyayı kapsar; yalnız `backup-user-data.mjs` kullanmak bildirimleri yedeklemez. diff --git a/server/zeyilname.mjs b/server/zeyilname.mjs index 03f95c2..80f204e 100644 --- a/server/zeyilname.mjs +++ b/server/zeyilname.mjs @@ -25,7 +25,7 @@ let DatabaseSync = null; try { ({ DatabaseSync } = await import('node:sqlite')); } catch { /* devre dışı */ } const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const DB_PATH = path.join(__dirname, 'data', 'takip.sqlite'); +const DB_PATH = path.join(process.env.IHALEIST_USER_DATA_DIR || path.join(__dirname,'data'), 'takip.sqlite'); const SCHEMA = ` CREATE TABLE IF NOT EXISTS takip_anlik ( ikn TEXT PRIMARY KEY, ilan_sayisi INTEGER, son_ilan_tarihi TEXT, ihale_tarih TEXT, @@ -42,8 +42,9 @@ let db = null; function open() { if (db) return db; if (!DatabaseSync) return null; - fs.mkdirSync(path.dirname(DB_PATH), { recursive: true }); + fs.mkdirSync(path.dirname(DB_PATH), { recursive: true, mode:0o700 }); db = new DatabaseSync(DB_PATH); + fs.chmodSync(DB_PATH,0o600); db.exec('PRAGMA journal_mode = WAL;'); db.exec(SCHEMA); return db; diff --git a/src/components/TenderDetailModal.jsx b/src/components/TenderDetailModal.jsx index 6b939e2..4ed9a71 100644 --- a/src/components/TenderDetailModal.jsx +++ b/src/components/TenderDetailModal.jsx @@ -422,8 +422,9 @@ export const TenderDetailModal = () => { onClick={handleExportICS} className="w-full bg-slate-100 hover:bg-slate-200 text-slate-700 font-bold py-2.5 px-3 rounded-lg text-xs flex items-center justify-center gap-2 border border-slate-300" > - Google / Outlook Takvime Ekle + Takvim Dosyasını İndir (.ics) +

Bu dosya otomatik güncellenmez. Tarih veya saat değişirse Google/Outlook takviminizdeki eski kaydı güncelleyin; yeni dosyayı içe aktarmak ikinci bir kayıt oluşturabilir.

diff --git a/src/services/tenderCalendar.js b/src/services/tenderCalendar.js index 4de0a85..d0ca444 100644 --- a/src/services/tenderCalendar.js +++ b/src/services/tenderCalendar.js @@ -15,7 +15,7 @@ export function tenderDeadline(t) { throw new Error('Geçersiz ihale tarihi veya saati.'); return new Date(Date.UTC(Number(year),Number(month)-1,Number(day),hour-3,minute)); } -const escapeICS = v => String(v || '').replace(/\\/g,'\\\\').replace(/\r?\n/g,'\\n').replace(/;/g,'\\;').replace(/,/g,'\\,'); +const escapeICS = v => String(v || '').replace(/\\/g,'\\\\').replace(/\r\n|\r|\n/g,'\\n').replace(/;/g,'\\;').replace(/,/g,'\\,'); const stamp = date => date.toISOString().replace(/[-:]/g,'').replace(/\.\d{3}Z$/,'Z'); function fold(line) { const result = []; let current = ''; @@ -31,9 +31,9 @@ export function createTenderICS(t, now = new Date()) { if (!uid) throw new Error('İhale kimliği gerekli.'); return ['BEGIN:VCALENDAR','VERSION:2.0','PRODID:-//Ihaleist//Tender Calendar//TR','CALSCALE:GREGORIAN', 'BEGIN:VEVENT',`UID:${uid}@ihaleist.com`,`DTSTAMP:${stamp(now)}`, - `DTSTART:${stamp(start)}`,`DTEND:${stamp(start)}`, + `DTSTART:${stamp(start)}`,'TRANSP:TRANSPARENT', `SUMMARY:${escapeICS('İhale son teklif: '+t.baslik)}`, - `DESCRIPTION:${escapeICS('İKN: '+t.ikn+' — '+t.kurum)}`, + `DESCRIPTION:${escapeICS('İKN: '+t.ikn+' — '+t.kurum+'\nBu tek seferlik takvim kaydı otomatik güncellenmez. İhale tarihini resmi kaynaktan doğrulayın; değişiklikte takviminizdeki eski kaydı güncelleyin.')}`, 'END:VEVENT','END:VCALENDAR',''].map(fold).join('\r\n'); } diff --git a/tests/calendar.test.mjs b/tests/calendar.test.mjs index 554fa34..d169271 100644 --- a/tests/calendar.test.mjs +++ b/tests/calendar.test.mjs @@ -47,3 +47,16 @@ test('month view includes late-month tenders and leap days', () => { assert.equal(calendarMonthDays('2027-02').length,28); assert.deepEqual(calendarMonthDays('2026-13'),[]); }); + +test('deadline ICS is a transparent instant, preserves UID across date changes and states update limits',()=>{ + const first=createTenderICS(t,new Date('2026-09-01T00:00:00Z')); + const changed=createTenderICS({...t,tarih:'12.09.2026',saat:'14:30'},new Date('2026-09-02T00:00:00Z')); + assert.equal(first.match(/^UID:.+$/m)[0],changed.match(/^UID:.+$/m)[0]); + assert.match(changed,/DTSTART:20260912T113000Z/); + assert.doesNotMatch(changed,/DTEND:|DURATION:/); + assert.match(changed,/TRANSP:TRANSPARENT/); + assert.match(changed.replace(/\r\n /g,''),/otomatik güncellenmez/); + const hostile=createTenderICS({...t,baslik:'A\rEND:VEVENT\rBEGIN:VEVENT\nB'}); + assert.equal(hostile.split('\r\n').filter(line=>line==='BEGIN:VEVENT').length,1); + assert.equal(hostile.split('\r\n').filter(line=>line==='END:VEVENT').length,1); +}); diff --git a/tests/notification-store.test.mjs b/tests/notification-store.test.mjs new file mode 100644 index 0000000..4ba589f --- /dev/null +++ b/tests/notification-store.test.mjs @@ -0,0 +1,24 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {execFileSync} from 'node:child_process'; +import {mkdtempSync,rmSync,existsSync,statSync} from 'node:fs'; +import {tmpdir} from 'node:os'; +import {join} from 'node:path'; +const url=new URL('../server/zeyilname.mjs',import.meta.url).href; +test('notifications live in configured user volume, persist, and scope read acknowledgements to owner',()=>{ + const dir=mkdtempSync(join(tmpdir(),'ihaleist-notifications-')); + const run=code=>JSON.parse(execFileSync(process.execPath,['--input-type=module','-e',`const notifications=await import(${JSON.stringify(url)});${code}`],{ + env:{...process.env,IHALEIST_USER_DATA_DIR:dir,IHALEIST_SHARED_DATA_DIR:join(dir,'shared')},encoding:'utf8',timeout:10000,stdio:['ignore','pipe','pipe'] + })); + try { + const initial=run(` + const id=notifications.bildirimYaz({email:'owner',tur:'zeyilname',ikn:'2026/1',baslik:'Tarih değişti',metin:'10 Eylül → 12 Eylül'}); + const foreign=notifications.okunduIsaretle('other',[id]); + console.log(JSON.stringify({id,foreign,owner:notifications.bildirimleriOku('owner'),other:notifications.bildirimleriOku('other')})); + `); + assert.equal(initial.foreign,0);assert.deepEqual(initial.other,[]);assert.equal(initial.owner[0].okundu,0); + assert.ok(existsSync(join(dir,'takip.sqlite')));assert.equal(statSync(join(dir,'takip.sqlite')).mode&0o777,0o600); + const next=run(`const changed=notifications.okunduIsaretle('owner',[${JSON.stringify(initial.id)}]);console.log(JSON.stringify({changed,rows:notifications.bildirimleriOku('owner')}));`); + assert.equal(next.changed,1);assert.equal(next.rows[0].okundu,1);assert.equal(next.rows[0].metin,'10 Eylül → 12 Eylül'); + }finally {rmSync(dir,{recursive:true,force:true});} +});