149 lines
5 KiB
JavaScript
149 lines
5 KiB
JavaScript
/**
|
|
* 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, '<').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 = '<div class="empty">No tagged domains found. Tag a domain in Zoraxy (e.g. "tracking") and refresh.</div>';
|
|
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 '<li class="domain source">' + esc(d) + ' <span class="badge-source">SOURCE</span></li>';
|
|
}
|
|
return '<li class="domain target">' + esc(d) + '</li>';
|
|
}).join('');
|
|
|
|
var noSource = !g.has_source
|
|
? '<div class="warn">No domain in this tag has headers yet. Set headers on one domain (via Zoraxy UI) to use it as the source.</div>'
|
|
: '';
|
|
|
|
var actions = g.has_source
|
|
? '<button class="btn btn-primary btn-apply">Apply to ' + targetCount + ' target(s)</button>' +
|
|
'<button class="btn btn-secondary btn-remove">Remove from targets</button>'
|
|
: '';
|
|
|
|
return (
|
|
'<div class="group" data-tag="' + escAttr(g.tag) + '" data-source="' + escAttr(g.source) + '">' +
|
|
'<h2 class="group-title">' + esc(g.tag) + '</h2>' +
|
|
noSource +
|
|
'<ul class="domain-list">' + domainsHtml + '</ul>' +
|
|
'<div class="group-actions">' + actions + '</div>' +
|
|
'<div class="result"></div>' +
|
|
'</div>'
|
|
);
|
|
}).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 = '<span class="ok">' + ok + ' applied</span>' + (fail ? ', <span class="err">' + fail + ' failed</span>' : '');
|
|
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 = '<span class="err">' + esc(msg) + '</span>';
|
|
}
|
|
});
|
|
}
|
|
|
|
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 = '<span class="ok">' + ok + ' removed</span>' + (fail ? ', <span class="err">' + fail + ' failed</span>' : '');
|
|
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 = '<span class="err">' + esc(msg) + '</span>';
|
|
}
|
|
});
|
|
}
|
|
|
|
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();
|