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;