feat: review uncertain weekly brief deliveries with audit history
This commit is contained in:
@@ -31,6 +31,7 @@ export function createBriefSender({ store, buildBrief, text, html, telegramConfi
|
||||
});
|
||||
if (!channels.length) { r.kanalsiz++; continue; }
|
||||
const batch = outbox?.prepare(briefWeekKey(clock()),user.email,channels.map(channel=>channel.payload));
|
||||
if (batch?.closed) { r.atlanan+=batch.count; continue; }
|
||||
if (batch?.changed) { r.bekleyen++; r.hata++; continue; }
|
||||
const failedChannels = new Set();
|
||||
for (const [position,channel] of channels.entries()) {
|
||||
@@ -38,7 +39,7 @@ export function createBriefSender({ store, buildBrief, text, html, telegramConfi
|
||||
// An earlier channel can await the network while the user disables briefs.
|
||||
if (store.getData(user.email, 'brief')?.kapali) { r.kapali++; break; }
|
||||
const claim = outbox?.claim(batch.id,position,clock().getTime());
|
||||
if (claim?.state === 'accepted') { r.atlanan++; continue; }
|
||||
if (claim?.state === 'accepted' || claim?.state === 'skipped') { r.atlanan++; continue; }
|
||||
if (claim && claim.state !== 'sending') {
|
||||
r.bekleyen++; failedChannels.add(channel.name);
|
||||
if (claim.state === 'uncertain') failed=true;
|
||||
|
||||
+40
-2
@@ -20,7 +20,8 @@ export function createBriefOutbox(directory) {
|
||||
chmodSync(file,0o600);
|
||||
db.exec(`PRAGMA busy_timeout=5000; PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL;
|
||||
CREATE TABLE IF NOT EXISTS brief_batches(id TEXT PRIMARY KEY,week TEXT NOT NULL,email TEXT NOT NULL,manifest TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS brief_messages(batch TEXT NOT NULL,position INTEGER NOT NULL,state TEXT NOT NULL,lease TEXT,lease_until INTEGER,receipt TEXT,error TEXT,PRIMARY KEY(batch,position));`);
|
||||
CREATE TABLE IF NOT EXISTS brief_messages(batch TEXT NOT NULL,position INTEGER NOT NULL,state TEXT NOT NULL,lease TEXT,lease_until INTEGER,receipt TEXT,error TEXT,PRIMARY KEY(batch,position));
|
||||
CREATE TABLE IF NOT EXISTS brief_reviews(batch TEXT NOT NULL,position INTEGER NOT NULL,actor TEXT NOT NULL,action TEXT NOT NULL,note TEXT NOT NULL,reviewed_at TEXT NOT NULL,PRIMARY KEY(batch,position));`);
|
||||
db.exec('BEGIN IMMEDIATE');
|
||||
const result=work(db);
|
||||
db.exec('COMMIT');
|
||||
@@ -34,6 +35,8 @@ export function createBriefOutbox(directory) {
|
||||
return transaction(db=>{
|
||||
const row=db.prepare('SELECT manifest FROM brief_batches WHERE id=?').get(id);
|
||||
if(row) {
|
||||
const remaining=db.prepare("SELECT COUNT(*) AS count FROM brief_messages WHERE batch=? AND state NOT IN ('accepted','skipped')").get(id).count;
|
||||
if(!remaining)return {id,closed:true,count:db.prepare('SELECT COUNT(*) AS count FROM brief_messages WHERE batch=?').get(id).count};
|
||||
const changed=row.manifest!==manifest;
|
||||
if(changed) db.prepare("UPDATE brief_messages SET state='uncertain',error=? WHERE batch=? AND state='pending'")
|
||||
.run('Haftalık özet içeriği veya alıcı kanalı değişti; yeniden gönderim öncesi inceleme gerekli.',id);
|
||||
@@ -49,7 +52,7 @@ export function createBriefOutbox(directory) {
|
||||
return transaction(db=>{
|
||||
const row=db.prepare('SELECT * FROM brief_messages WHERE batch=? AND position=?').get(batch,position);
|
||||
if(!row)throw new Error('Özet teslimat kaydı bulunamadı.');
|
||||
if(row.state==='accepted')return {state:'accepted'};
|
||||
if(row.state==='accepted'||row.state==='skipped')return {state:row.state};
|
||||
if(row.state==='uncertain')return {state:'uncertain'};
|
||||
if(row.state==='sending') {
|
||||
if(row.lease_until>clock)return {state:'busy'};
|
||||
@@ -73,6 +76,41 @@ export function createBriefOutbox(directory) {
|
||||
return transaction(db=>db.prepare("UPDATE brief_messages SET state='uncertain',error=? WHERE batch=? AND position=? AND lease=? AND state='sending'")
|
||||
.run(String(error?.message || error),claim.batch,claim.position,claim.lease).changes===1);
|
||||
},
|
||||
queue(clock=Date.now()) {
|
||||
return transaction(db=>{
|
||||
db.prepare("UPDATE brief_messages SET state='uncertain',error=? WHERE state='sending' AND lease_until<=?")
|
||||
.run('Kesintiye uğrayan gönderim; sağlayıcı kaydı incelenmeli.',clock);
|
||||
return db.prepare(`SELECT b.week,b.email,b.manifest,m.*,r.actor,r.action,r.note,r.reviewed_at
|
||||
FROM brief_messages m JOIN brief_batches b ON b.id=m.batch
|
||||
LEFT JOIN brief_reviews r ON r.batch=m.batch AND r.position=m.position
|
||||
WHERE m.state!='accepted' OR r.action IS NOT NULL
|
||||
ORDER BY b.week DESC,b.email,m.position`).all().map(row=>{
|
||||
const message=JSON.parse(row.manifest)[row.position];
|
||||
return {id:`${row.batch}:${row.position}`,to:message.to || row.email,
|
||||
subject:`Haftalık özet · ${row.week} · ${message.channel === 'telegram' ? 'Telegram' : 'E-posta'} · Mesaj ${row.position+1}`,
|
||||
state:row.action==='confirmed_sent'?'sent_manual':row.state,error:row.error,providerId:row.receipt,
|
||||
review:row.action?{actor:row.actor,action:row.action,note:row.note,at:row.reviewed_at}:null};
|
||||
});
|
||||
});
|
||||
},
|
||||
resolve(id,actor,input) {
|
||||
const match=typeof id==='string' && /^([a-f0-9]{64}):(\d+)$/.exec(id);
|
||||
if(!match || !Number.isSafeInteger(Number(match[2])) || typeof actor!=='string' || !actor.trim() ||
|
||||
!['confirmed_sent','skip'].includes(input?.action) || typeof input.note!=='string' || input.note.trim().length<20 || input.note.length>2000)
|
||||
throw new Error('Karar ve en az 20 karakterlik kontrol açıklaması gerekli.');
|
||||
const sent=input.action==='confirmed_sent';
|
||||
if(sent && (input.confirmed!==true || typeof input.providerId!=='string' || !input.providerId.trim() || input.providerId.length>200))
|
||||
throw new Error('Sağlayıcı gönderim kimliği ve kontrol onayı gerekli.');
|
||||
const batch=match[1],position=Number(match[2]);
|
||||
return transaction(db=>{
|
||||
const row=db.prepare("SELECT state FROM brief_messages WHERE batch=? AND position=?").get(batch,position);
|
||||
if(row?.state!=='uncertain')throw Object.assign(new Error('Kayıt artık inceleme beklemiyor. Listeyi yenileyin.'),{status:409});
|
||||
db.prepare('INSERT INTO brief_reviews VALUES (?,?,?,?,?,?)').run(batch,position,actor,input.action,input.note.trim(),new Date().toISOString());
|
||||
db.prepare('UPDATE brief_messages SET state=?,receipt=?,error=NULL WHERE batch=? AND position=?')
|
||||
.run(sent?'accepted':'skipped',sent?input.providerId.trim():null,batch,position);
|
||||
return {id,state:sent?'sent_manual':'skipped'};
|
||||
});
|
||||
},
|
||||
inspect() {
|
||||
return transaction(db=>db.prepare('SELECT b.week,b.email,m.* FROM brief_messages m JOIN brief_batches b ON b.id=m.batch ORDER BY b.week DESC,b.email,m.position').all());
|
||||
},
|
||||
|
||||
+2
-1
@@ -57,10 +57,11 @@ export function briefHtml(b) {
|
||||
<p style="margin-top:20px;font-size:12px;color:#94a3b8">Bu brief her Pazartesi gönderilir; Bildirimler sekmesinden kapatabilirsiniz.</p></div>`;
|
||||
}
|
||||
|
||||
export const briefOutbox = createBriefOutbox(process.env.IHALEIST_USER_DATA_DIR || path.join(__dirname,'data'));
|
||||
export const haftalikBriefGonder = createBriefSender({
|
||||
store, buildBrief:briefOlustur, text:briefMetni, html:briefHtml,
|
||||
telegramConfigured, sendTelegram, emailConfigured, sendEmail,
|
||||
outbox:createBriefOutbox(process.env.IHALEIST_USER_DATA_DIR || path.join(__dirname,'data')),
|
||||
outbox:briefOutbox,
|
||||
});
|
||||
|
||||
export function briefScheduleKey(now = new Date()) {
|
||||
|
||||
+12
-1
@@ -58,7 +58,7 @@ import { startScheduler, schedulerStatus } from './scheduler.mjs';
|
||||
import { searchKik, kararMetni } from './kik.mjs';
|
||||
import { documentUrl, ilanMetni, documentText, browserAvailable } from './documents.mjs';
|
||||
import { ihaleRss, sonucRss, mcpIstek, yaklasanIhaleler } from './feeds-mcp.mjs';
|
||||
import { briefOlustur, briefMetni, haftalikBriefGonder } from './brief.mjs';
|
||||
import { briefOlustur, briefMetni, haftalikBriefGonder, briefOutbox } from './brief.mjs';
|
||||
import { belgeyeSor, teklifDenetle, teklifTaslak, ihaleMetni, gereklilikMetni } from './ai-teklif.mjs';
|
||||
import { subscribe, list as listSubs, remove as removeSub, deliveryQueue, resolveDelivery, available as notifyAvailable } from './notify-store.mjs';
|
||||
import { bildirimleriIsle } from './notifier.mjs';
|
||||
@@ -1534,6 +1534,17 @@ const server = http.createServer(async (req, res) => {
|
||||
}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/outbox') {
|
||||
const user=userFromAuthHeader(req.headers.authorization);
|
||||
if(!user)return json(res,401,{success:false,error:'Oturum gerekli.'});
|
||||
if(!user.isAdmin)return json(res,403,{success:false,error:'Yönetici yetkisi gerekli.'});
|
||||
try {
|
||||
if(req.method==='GET')return json(res,200,{success:true,deliveries:briefOutbox.queue()});
|
||||
if(req.method==='POST'){const body=await readBody(req);return json(res,200,{success:true,delivery:briefOutbox.resolve(body.id,user.email,body)});}
|
||||
return json(res,405,{success:false,error:'Desteklenmeyen metot.'});
|
||||
}catch(error){return json(res,error.status||400,{success:false,error:error.message});}
|
||||
}
|
||||
|
||||
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' });
|
||||
|
||||
Reference in New Issue
Block a user