intra_monitor/public/js/sse.js
2026-03-23 22:38:53 +03:00

129 lines
5.3 KiB
JavaScript

function showToast(text, type='info', duration=4000) {
const wrap=document.getElementById('toast-wrap');
const el=document.createElement('div');
const cls={mark:'',note:'success',warn:'warn',reload:'info','mention-t':'mention',success:'success',info:'info',warn:'warn',mention:'mention'}[type]||'';
el.className=`toast ${cls}`; el.textContent=text;
wrap.appendChild(el);
setTimeout(()=>el.remove(),duration);
}
function playMentionSound() {
try {
const ctx=new(window.AudioContext||window.webkitAudioContext)();
const o=ctx.createOscillator(),g=ctx.createGain();
o.connect(g);g.connect(ctx.destination);
o.type='sine';o.frequency.value=880;
g.gain.setValueAtTime(0.25,ctx.currentTime);
g.gain.exponentialRampToValueAtTime(0.001,ctx.currentTime+0.35);
o.start(ctx.currentTime);o.stop(ctx.currentTime+0.35);
} catch {}
}
let sseSource=null;
let sseReconnectTimer=null;
function startSSE() {
if(sseReconnectTimer){clearTimeout(sseReconnectTimer);sseReconnectTimer=null;}
if(sseSource){sseSource.close();sseSource=null;}
if(!authToken) return;
sseSource=new EventSource(`/api/events?token=${encodeURIComponent(authToken)}`);
sseSource.addEventListener('mark',e=>{
const d=JSON.parse(e.data);
if(!marksCache[d.task_id]) marksCache[d.task_id]={};
marksCache[d.task_id][d.mark_type]={active:d.value,by:d.by,at:d.at};
// Сбрасываем все метки, которые сервер очистил в том же запросе
(d.cleared_marks||[]).forEach(m=>{
marksCache[d.task_id][m]={...(marksCache[d.task_id][m]||{}),active:false};
});
// Тост только для действий других пользователей
if(d.by!==currentUser?.username){
const markCfg=MARKS_CONFIG.find(m=>m.key===d.mark_type);
const label=markCfg?markCfg.sseLabel:d.mark_type;
showToast(`👤 ${d.by}: ${d.value?`✓ «${label}»`:`снял «${label}»`} #${taskNumFromId(d.task_id)}`,'info');
}
renderTable();
refreshDetailIfOpen(d.task_id);
});
sseSource.addEventListener('note',e=>{
const d=JSON.parse(e.data);
if(d.text) notesCache[d.task_id]={text:d.text,by:d.by,at:d.at};
else delete notesCache[d.task_id];
// Тост только для действий других пользователей
if(d.by!==currentUser?.username){
showToast(`📝 ${d.by}: ${d.text?'обновил заметку':'удалил заметку'} #${taskNumFromId(d.task_id)}`,'success');
}
renderTable();
refreshDetailIfOpen(d.task_id);
});
sseSource.addEventListener('comment_new',e=>{
const d=JSON.parse(e.data);
const tid=String(d.comment.task_id);
if(!commentsCache[tid]) commentsCache[tid]=[];
commentsCache[tid].push(d.comment);
if(typeof commentCounts==='object') commentCounts[tid]=(commentsCache[tid]||[]).length;
showToast(`💬 ${d.comment.author}: комментарий к #${d.task_number}`,'success');
renderTable();
refreshDetailIfOpen(tid);
});
sseSource.addEventListener('comment_edit',e=>{
const d=JSON.parse(e.data);
const tid=String(d.comment.task_id);
if(commentsCache[tid]) commentsCache[tid]=commentsCache[tid].map(c=>c.id===d.comment.id?d.comment:c);
refreshDetailIfOpen(tid);
});
sseSource.addEventListener('comment_delete',e=>{
const d=JSON.parse(e.data);
const tid=String(d.task_id);
if(commentsCache[tid]) commentsCache[tid]=commentsCache[tid].filter(c=>c.id!==d.comment_id);
if(typeof commentCounts==='object') commentCounts[tid]=(commentsCache[tid]||[]).length;
renderTable();
refreshDetailIfOpen(tid);
});
sseSource.addEventListener('intra_addfield',e=>{
const d=JSON.parse(e.data);
const item=allData.find((x)=>String(x.task.id)===String(d.task_id));
if(item) item.task._addfield_status=d.value||null;
if(d.by!==currentUser?.username){
showToast(`🔗 ${d.by}: служ. статус → «${d.value||'—'}» #${taskNumFromId(d.task_id)}`,'info');
}
renderTable();
refreshDetailIfOpen(d.task_id);
});
sseSource.addEventListener('tasks_updated',e=>{
const d=JSON.parse(e.data);
const incomingVersion=Number(d.cache_version||0);
if(incomingVersion&&incomingVersion===lastKnownCacheVersion) return;
if(incomingVersion) lastKnownCacheVersion=incomingVersion;
showToast(`🔄 Обновление: ${d.count} заявок`,'info',4000);
reloadData();
});
sseSource.addEventListener('mention',e=>{
const d=JSON.parse(e.data);
mentionsData.unshift({task_id:d.task_id,mentioned_user:currentUser.username,mentioned_by:d.by,created_at:d.at,read_at:null});
mentionUnread=d.unread_count;
renderMentionsBell();
playMentionSound();
showToast(`🔔 ${d.by} упомянул вас в заявке #${d.task_number}`,'mention',8000);
});
sseSource.onerror=()=>{
// Браузер сам переподключается при CONNECTING (0/1), нам нужно только при CLOSED (2)
if(!sseSource || sseSource.readyState !== EventSource.CLOSED) return;
if(sseReconnectTimer) return;
sseReconnectTimer=setTimeout(()=>{
sseReconnectTimer=null;
if(authToken) startSSE();
},5000);
};
}
function stopSSE(){
if(sseReconnectTimer){clearTimeout(sseReconnectTimer);sseReconnectTimer=null;}
if(sseSource){sseSource.close();sseSource=null;}
}