/** * Header Tagger — client-side logic. * Uses Zoraxy's $.cjax() for CSRF-safe AJAX. */ function showToast(message, type) { var toast = document.getElementById('toast'); toast.textContent = message; toast.className = 'toast ' + (type || ''); toast.classList.remove('hidden'); setTimeout(function () { toast.classList.add('hidden'); }, 4000); } function esc(s) { if (!s) return ''; return s.replace(/&/g, '&').replace(//g, '>'); } function escAttr(s) { if (!s) return ''; return s.replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, '''); } function renderGroups(data) { var container = document.getElementById('groups'); var groups = data.groups || []; if (groups.length === 0) { container.innerHTML = '
No tagged domains found. Tag a domain in Zoraxy (e.g. "tracking") and refresh.
'; return; } var html = groups.map(function (g) { var targetCount = g.domains.filter(function (d) { return d !== g.source; }).length; var domainsHtml = g.domains.map(function (d) { var isSource = (d === g.source); if (isSource) { return '
  • ' + esc(d) + ' SOURCE
  • '; } return '
  • ' + esc(d) + '
  • '; }).join(''); var noSource = !g.has_source ? '
    No domain in this tag has headers yet. Set headers on one domain (via Zoraxy UI) to use it as the source.
    ' : ''; var actions = g.has_source ? '' + '' : ''; return ( '
    ' + '

    ' + esc(g.tag) + '

    ' + noSource + '' + '
    ' + actions + '
    ' + '
    ' + '
    ' ); }).join(''); container.innerHTML = html; } function loadGroups() { $.get('./api/groups', function (data) { renderGroups(data); }).fail(function () { showToast('Failed to load tags', 'error'); }); } // Collect all target domains (all non-source domains in the tag). function getTargets(group) { var targets = []; var items = group.querySelectorAll('.domain.target'); for (var i = 0; i < items.length; i++) { targets.push(items[i].textContent.trim()); } return targets; } function applyGroup(group) { var tag = group.dataset.tag; var source = group.dataset.source; var targets = getTargets(group); var resultEl = group.querySelector('.result'); if (!source) { showToast('No source domain with headers', 'error'); return; } if (targets.length === 0) { showToast('No target domains in this tag', 'error'); return; } resultEl.textContent = 'Applying...'; $.cjax({ url: './api/apply', type: 'POST', dataType: 'json', data: { tag: tag, source: source, targets: targets }, success: function (data) { var ok = (data.results || []).filter(function (r) { return r.success; }).length; var fail = (data.results || []).filter(function (r) { return !r.success; }).length; resultEl.innerHTML = '' + ok + ' applied' + (fail ? ', ' + fail + ' failed' : ''); loadGroups(); }, error: function (xhr) { var msg = 'Apply failed'; try { var r = JSON.parse(xhr.responseText); msg = r.error || r.message || msg; } catch(e) {} resultEl.innerHTML = '' + esc(msg) + ''; } }); } function removeGroup(group) { var source = group.dataset.source; var targets = getTargets(group); var resultEl = group.querySelector('.result'); if (!source) { showToast('No source domain', 'error'); return; } if (targets.length === 0) { showToast('No target domains in this tag', 'error'); return; } resultEl.textContent = 'Removing...'; $.cjax({ url: './api/remove', type: 'POST', dataType: 'json', data: { source: source, targets: targets }, success: function (data) { var ok = (data.results || []).filter(function (r) { return r.success; }).length; var fail = (data.results || []).filter(function (r) { return !r.success; }).length; resultEl.innerHTML = '' + ok + ' removed' + (fail ? ', ' + fail + ' failed' : ''); loadGroups(); }, error: function (xhr) { var msg = 'Remove failed'; try { var r = JSON.parse(xhr.responseText); msg = r.error || r.message || msg; } catch(e) {} resultEl.innerHTML = '' + esc(msg) + ''; } }); } document.getElementById('groups').addEventListener('click', function (e) { var group = e.target.closest('.group'); if (!group) return; if (e.target.closest('.btn-apply')) applyGroup(group); if (e.target.closest('.btn-remove')) removeGroup(group); }); loadGroups();