Skip to content

Commit ff9f04e

Browse files
dashboard: add adlist filter dropdown to Top Blocked Domains
Allow users to filter the Top Blocked Domains table by subscription list (adlist) directly from the dashboard. - index.lp: add a <select id="ad-frequency-list-filter"> in the Top Blocked Domains box header - scripts/js/index.js: - populateBlockedListFilter(): fetch /api/lists, populate dropdown with enabled blocklists - updateTopDomainsTable(): when a specific list is selected, fetch a larger pool of top domains and filter client-side via /api/search/<domain> to identify which adlist each domain belongs to, then render the top 10 matches; falls back to the unfiltered endpoint when "All lists" is selected - wire dropdown change event to refresh the table Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 7a17e01 commit ff9f04e

2 files changed

Lines changed: 137 additions & 1 deletion

File tree

index.lp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,11 @@ mg.include('scripts/lua/header_authenticated.lp','r')
191191
<div class="box" id="ad-frequency">
192192
<div class="box-header with-border">
193193
<h3 class="box-title">Top Blocked Domains</h3>
194+
<div class="pull-right" style="width:45%;max-width:280px;">
195+
<select id="ad-frequency-list-filter" class="form-control input-sm" title="Filter by subscription list">
196+
<option value="">All lists</option>
197+
</select>
198+
</div>
194199
</div>
195200
<!-- /.box-header -->
196201
<div class="box-body">

scripts/js/index.js

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,68 @@ function updateTopClientsTable(blocked) {
358358
});
359359
}
360360

361+
// Populate the subscription-list dropdown on the "Top Blocked Domains" card.
362+
// Only enabled blocklists are shown; allowlists are excluded.
363+
function populateBlockedListFilter() {
364+
const select = $("#ad-frequency-list-filter");
365+
// Preserve any previously selected value
366+
const previousVal = select.val();
367+
368+
$.getJSON(document.body.dataset.apiurl + "/lists", data => {
369+
// Remove all options except the first "All lists" placeholder
370+
select.find("option:not(:first)").remove();
371+
372+
if (!data.lists) return;
373+
374+
data.lists.forEach(list => {
375+
if (list.type === "block" && list.enabled) {
376+
const label =
377+
list.address.length > 50 ? list.address.substring(0, 47) + "..." : list.address;
378+
select.append($("<option>", { value: list.id, text: label, title: list.address }));
379+
}
380+
});
381+
382+
// Restore selection if the same list is still present
383+
if (previousVal) select.val(previousVal);
384+
});
385+
}
386+
387+
// Fetch the set of adlist IDs a domain belongs to via the search API.
388+
// Returns a Promise resolving to a Set of numeric IDs.
389+
// Response shape: { search: { gravity: [ { id: N, address: "...", ... }, ... ] } }
390+
function fetchDomainAdlistIds(domain) {
391+
return new Promise(resolve => {
392+
$.getJSON(document.body.dataset.apiurl + "/search/" + encodeURIComponent(domain))
393+
.then(data => {
394+
const ids = new Set();
395+
if (data.search && Array.isArray(data.search.gravity)) {
396+
data.search.gravity.forEach(entry => {
397+
if (typeof entry.id === "number") ids.add(entry.id);
398+
});
399+
}
400+
resolve(ids);
401+
})
402+
.fail(() => resolve(new Set()));
403+
});
404+
}
405+
406+
// Render a list of domain items into the blocked-domains table.
407+
function renderBlockedDomainRows(items, sum, domaintable) {
408+
items.forEach(item => {
409+
const domain = encodeURIComponent(item.domain);
410+
const urlText = domain === "" ? "." : item.domain;
411+
const url = '<a href="queries?domain=' + domain + '&upstream=blocklist">' + urlText + "</a>";
412+
const percentage = (item.count / sum) * 100;
413+
domaintable.append(
414+
"<tr> " +
415+
utils.addTD(url) +
416+
utils.addTD(item.count) +
417+
utils.addTD(utils.colorBar(percentage, sum, "queries-blocked")) +
418+
"</tr> "
419+
);
420+
});
421+
}
422+
361423
function updateTopDomainsTable(blocked) {
362424
let api;
363425
let style;
@@ -366,12 +428,73 @@ function updateTopDomainsTable(blocked) {
366428
let overlay;
367429
let domaintable;
368430
if (blocked) {
369-
api = document.body.dataset.apiurl + "/stats/top_domains?blocked=true";
370431
style = "queries-blocked";
371432
table = $("#ad-frequency");
372433
tablecontent = $("#ad-frequency td").parent();
373434
overlay = $("#ad-frequency .overlay");
374435
domaintable = $("#ad-frequency").find("tbody:last");
436+
437+
const selectedList = $("#ad-frequency-list-filter").val();
438+
const adlistId = selectedList ? parseInt(selectedList, 10) : -1;
439+
440+
if (adlistId >= 0) {
441+
// ── Filtered mode ────────────────────────────────────────────────────
442+
// Client-side filtering: fetch a larger pool of blocked domains, then
443+
// use /api/search/<domain> for each to check which adlist it belongs
444+
// to. This works with the stock FTL binary (no rebuild required).
445+
// When FTL is eventually rebuilt with the ?list= server-side param,
446+
// this code can be replaced by a single API call.
447+
const POOL_SIZE = 25;
448+
tablecontent.remove();
449+
overlay.show();
450+
451+
$.getJSON(
452+
document.body.dataset.apiurl + "/stats/top_domains?blocked=true&count=" + POOL_SIZE,
453+
data => {
454+
const domains = data.domains || [];
455+
const sum = data.blocked_queries;
456+
457+
if (domains.length === 0) {
458+
domaintable.append('<tr><td colspan="3" class="text-center">- No data -</td></tr>');
459+
overlay.hide();
460+
return;
461+
}
462+
463+
// Check every domain in parallel; then filter and render.
464+
Promise.all(
465+
domains.map(item =>
466+
fetchDomainAdlistIds(item.domain).then(ids => ({
467+
item,
468+
inList: ids.has(adlistId),
469+
}))
470+
)
471+
).then(results => {
472+
const matched = results
473+
.filter(r => r.inList)
474+
.slice(0, 10)
475+
.map(r => r.item);
476+
477+
tablecontent.remove();
478+
if (matched.length === 0) {
479+
domaintable.append(
480+
'<tr><td colspan="3" class="text-center">- No blocked domains from this list -</td></tr>'
481+
);
482+
} else {
483+
renderBlockedDomainRows(matched, sum, domaintable);
484+
}
485+
overlay.hide();
486+
});
487+
}
488+
).fail(data => {
489+
apiFailure(data);
490+
});
491+
492+
// Early return — rendering happens inside the async callbacks above.
493+
return;
494+
}
495+
496+
// ── Unfiltered mode (All lists) ───────────────────────────────────────
497+
api = document.body.dataset.apiurl + "/stats/top_domains?blocked=true";
375498
} else {
376499
api = document.body.dataset.apiurl + "/stats/top_domains";
377500
style = "queries-permitted";
@@ -828,6 +951,14 @@ $(() => {
828951

829952
// Initialize privacy level before loading any data that depends on it
830953
initPrivacyLevel().then(() => {
954+
// Populate the subscription-list dropdown, then load the top lists
955+
populateBlockedListFilter();
956+
957+
// Re-fetch the blocked table whenever the user changes the list filter
958+
$("#ad-frequency-list-filter").on("change", () => {
959+
updateTopDomainsTable(true);
960+
});
961+
831962
// After privacy level is initialized, load the top lists
832963
updateTopLists();
833964
});

0 commit comments

Comments
 (0)