Spaces:
Sleeping
Sleeping
File size: 4,742 Bytes
70d9e6c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | /** In-app notification bell — polls completed detection jobs (FR-05). */
const SEEN_JOBS_KEY = 'dda_seen_job_ids';
const POLL_MS = 30000;
function getSeenJobIds() {
try {
return new Set(JSON.parse(localStorage.getItem(SEEN_JOBS_KEY) || '[]'));
} catch (_) {
return new Set();
}
}
function saveSeenJobIds(ids) {
try {
localStorage.setItem(SEEN_JOBS_KEY, JSON.stringify([...ids].slice(-200)));
} catch (_) {}
}
function markJobSeen(jobId) {
const seen = getSeenJobIds();
seen.add(String(jobId));
saveSeenJobIds(seen);
refreshNotifications();
}
function formatNotifyTime(iso) {
if (!iso) return '';
try {
return new Date(iso).toLocaleString(undefined, { dateStyle: 'short', timeStyle: 'short' });
} catch (_) {
return iso;
}
}
async function fetchNotifyJobs() {
try {
const data = await ddaApi('GET', '/api/dda/jobs?limit=25');
return data.jobs || [];
} catch (_) {
return [];
}
}
function updateNotifyBadge(jobs) {
const badge = document.getElementById('dda-notify-badge');
if (!badge) return;
const seen = getSeenJobIds();
const unread = jobs.filter((j) => j.status === 'completed' && j.runId && !seen.has(String(j.id)));
if (unread.length) {
badge.textContent = unread.length > 9 ? '9+' : String(unread.length);
badge.classList.remove('hidden');
} else {
badge.classList.add('hidden');
}
return unread;
}
function renderNotifyPanel(jobs) {
const panel = document.getElementById('dda-notify-panel');
if (!panel) return;
const seen = getSeenJobIds();
const items = jobs.filter((j) => ['completed', 'failed', 'running', 'queued'].includes(j.status));
if (!items.length) {
panel.innerHTML = '<p class="dim dda-notify-empty">No recent jobs.</p>';
return;
}
panel.innerHTML = items.slice(0, 12).map((j) => {
const unread = j.status === 'completed' && !seen.has(String(j.id));
const title = j.title || `${j.basePath || 'Base'} vs ${j.comparisonPath || 'Comparison'}`;
const pct = j.report?.changePercentage;
const meta = j.status === 'completed' && pct != null ? `${pct.toFixed(2)}% change` : j.status;
return `
<button type="button" class="dda-notify-item${unread ? ' unread' : ''}" data-job-id="${j.id}" data-run-id="${j.runId || ''}" data-status="${j.status}">
<span class="dda-notify-item-title">${title}</span>
<span class="dda-notify-item-meta">${formatNotifyTime(j.completedAt || j.createdAt)} · ${meta}</span>
</button>`;
}).join('');
panel.querySelectorAll('.dda-notify-item').forEach((btn) => {
btn.addEventListener('click', async () => {
markJobSeen(btn.dataset.jobId);
const status = btn.dataset.status;
const runId = btn.dataset.runId;
closeNotifyPanel();
if (status === 'completed' && runId) {
if (typeof showDdaResult === 'function') {
try {
const data = await ddaApi('GET', `/api/history/${runId}`);
showDdaResult(data);
} catch (_) {
window.open(`/dda/reports/${runId}`, '_blank');
}
} else {
window.open(`/dda/reports/${runId}`, '_blank');
}
} else if (status === 'failed') {
if (typeof showDdaError === 'function') showDdaError('Detection job failed. See Reports tab for details.');
document.querySelector('.dda-tab[data-tab="reports"]')?.click();
} else {
document.querySelector('.dda-tab[data-tab="reports"]')?.click();
}
});
});
}
function closeNotifyPanel() {
document.getElementById('dda-notify-panel')?.classList.add('hidden');
}
async function refreshNotifications() {
const jobs = await fetchNotifyJobs();
updateNotifyBadge(jobs);
const panel = document.getElementById('dda-notify-panel');
if (panel && !panel.classList.contains('hidden')) {
renderNotifyPanel(jobs);
}
return jobs;
}
function initNotifications() {
const btn = document.getElementById('dda-notify-btn');
if (!btn) return;
btn.addEventListener('click', async (e) => {
e.stopPropagation();
const panel = document.getElementById('dda-notify-panel');
const wasHidden = panel?.classList.contains('hidden');
if (wasHidden) {
const jobs = await fetchNotifyJobs();
renderNotifyPanel(jobs);
panel?.classList.remove('hidden');
} else {
closeNotifyPanel();
}
});
document.addEventListener('click', (e) => {
if (e.target.closest('.dda-notify-wrap')) return;
closeNotifyPanel();
});
refreshNotifications();
setInterval(refreshNotifications, POLL_MS);
}
document.addEventListener('DOMContentLoaded', initNotifications);
window.markDdaJobSeen = markJobSeen;
window.refreshDdaNotifications = refreshNotifications;
|