Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions index.lp
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,11 @@ mg.include('scripts/lua/header_authenticated.lp','r')
<div class="box" id="ad-frequency">
<div class="box-header with-border">
<h3 class="box-title">Top Blocked Domains</h3>
<div class="pull-right" style="width:45%;max-width:280px;">
<select id="ad-frequency-list-filter" class="form-control input-sm" title="Filter by subscription list">
<option value="">All lists</option>
</select>
</div>
</div>
<!-- /.box-header -->
<div class="box-body">
Expand Down
135 changes: 134 additions & 1 deletion scripts/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,69 @@ function updateTopClientsTable(blocked) {
});
}

// Populate the subscription-list dropdown on the "Top Blocked Domains" card.
// Only enabled blocklists are shown; allowlists are excluded.
function populateBlockedListFilter() {
const select = $("#ad-frequency-list-filter");
// Preserve any previously selected value
const previousVal = select.val();

$.getJSON(document.body.dataset.apiurl + "/lists", data => {
// Remove all options except the first "All lists" placeholder
select.find("option:not(:first)").remove();

if (!data.lists) return;

for (const list of data.lists) {
if (list.type === "block" && list.enabled) {
const label =
list.address.length > 50 ? list.address.substring(0, 47) + "..." : list.address;
select.append($("<option>", { value: list.id, text: label, title: list.address }));
}
}

// Restore selection if the same list is still present
if (previousVal) select.val(previousVal);
});
}

// Fetch the set of adlist IDs a domain belongs to via the search API.
// Returns a Promise resolving to a Set of numeric IDs.
// Response shape: { search: { gravity: [ { id: N, address: "...", ... }, ... ] } }
function fetchDomainAdlistIds(domain) {
return new Promise(resolve => {
$.getJSON(document.body.dataset.apiurl + "/search/" + encodeURIComponent(domain))
.then(data => {
const ids = new Set();
if (data.search && Array.isArray(data.search.gravity)) {
for (const entry of data.search.gravity) {
if (typeof entry.id === "number") ids.add(entry.id);
}
}

resolve(ids);
})
.fail(() => resolve(new Set()));
});
}

// Render a list of domain items into the blocked-domains table.
function renderBlockedDomainRows(items, sum, domaintable) {
for (const item of items) {
const domain = encodeURIComponent(item.domain);
const urlText = domain === "" ? "." : item.domain;
const url = '<a href="queries?domain=' + domain + '&upstream=blocklist">' + urlText + "</a>";
const percentage = (item.count / sum) * 100;
domaintable.append(
"<tr> " +
utils.addTD(url) +
utils.addTD(item.count) +
utils.addTD(utils.colorBar(percentage, sum, "queries-blocked")) +
"</tr> "
);
}
}

function updateTopDomainsTable(blocked) {
let api;
let style;
Expand All @@ -366,12 +429,74 @@ function updateTopDomainsTable(blocked) {
let overlay;
let domaintable;
if (blocked) {
api = document.body.dataset.apiurl + "/stats/top_domains?blocked=true";
style = "queries-blocked";
table = $("#ad-frequency");
tablecontent = $("#ad-frequency td").parent();
overlay = $("#ad-frequency .overlay");
domaintable = $("#ad-frequency").find("tbody:last");

const selectedList = $("#ad-frequency-list-filter").val();
const adlistId = selectedList ? Number.parseInt(selectedList, 10) : -1;

if (adlistId >= 0) {
// ── Filtered mode ────────────────────────────────────────────────────
// Client-side filtering: fetch a larger pool of blocked domains, then
// use /api/search/<domain> for each to check which adlist it belongs
// to. This works with the stock FTL binary (no rebuild required).
// When FTL is eventually rebuilt with the ?list= server-side param,
// this code can be replaced by a single API call.
const POOL_SIZE = 25;
tablecontent.remove();
overlay.show();

$.getJSON(
document.body.dataset.apiurl + "/stats/top_domains?blocked=true&count=" + POOL_SIZE,
data => {
const domains = data.domains || [];
const sum = data.blocked_queries;

if (domains.length === 0) {
domaintable.append('<tr><td colspan="3" class="text-center">- No data -</td></tr>');
overlay.hide();
return;
}

// Check every domain in parallel; then filter and render.
Promise.all(
domains.map(item =>
fetchDomainAdlistIds(item.domain).then(ids => ({
item,
inList: ids.has(adlistId),
}))
)
).then(results => {
const matched = results
.filter(r => r.inList)
.slice(0, 10)
.map(r => r.item);

tablecontent.remove();
if (matched.length === 0) {
domaintable.append(
'<tr><td colspan="3" class="text-center">- No blocked domains from this list -</td></tr>'
);
} else {
renderBlockedDomainRows(matched, sum, domaintable);
}

overlay.hide();
});
}
).fail(data => {
apiFailure(data);
});

// Early return — rendering happens inside the async callbacks above.
return;
}

// ── Unfiltered mode (All lists) ───────────────────────────────────────
api = document.body.dataset.apiurl + "/stats/top_domains?blocked=true";
} else {
api = document.body.dataset.apiurl + "/stats/top_domains";
style = "queries-permitted";
Expand Down Expand Up @@ -828,6 +953,14 @@ $(() => {

// Initialize privacy level before loading any data that depends on it
initPrivacyLevel().then(() => {
// Populate the subscription-list dropdown, then load the top lists
populateBlockedListFilter();

// Re-fetch the blocked table whenever the user changes the list filter
$("#ad-frequency-list-filter").on("change", () => {
updateTopDomainsTable(true);
});

// After privacy level is initialized, load the top lists
updateTopLists();
});
Expand Down
Loading