add domain check endpoint and dashboard search box
GET /blocking/check/{domain} — returns whether a domain is blocked,
the reason (exact match, parent domain, allowlist, disabled), and
the matching rule. Dashboard sidebar has a "Check Domain" search
box with inline results and one-click allow button.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -519,6 +519,22 @@ body {
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="sidebar">
|
||||
<!-- Blocklist check -->
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">Check Domain</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form class="override-form" onsubmit="return checkDomain(event)" style="margin-bottom:0;border-bottom:none;padding-bottom:0;">
|
||||
<div class="override-form-row">
|
||||
<input type="text" id="checkDomain" placeholder="Is this domain blocked?" required style="flex:3">
|
||||
<button type="submit" class="btn" style="background:var(--violet);color:white;flex-shrink:0;">Check</button>
|
||||
</div>
|
||||
</form>
|
||||
<div id="checkResult" style="display:none;margin-top:0.6rem;padding:0.5rem 0.6rem;border-radius:5px;font-family:var(--font-mono);font-size:0.72rem;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active overrides -->
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
@@ -834,6 +850,35 @@ async function allowDomain(domain) {
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
async function checkDomain(event) {
|
||||
event.preventDefault();
|
||||
const domain = document.getElementById('checkDomain').value.trim();
|
||||
const el = document.getElementById('checkResult');
|
||||
if (!domain) return false;
|
||||
try {
|
||||
const result = await fetchJSON('/blocking/check/' + encodeURIComponent(domain));
|
||||
el.style.display = 'block';
|
||||
if (result.blocked) {
|
||||
el.style.background = 'rgba(181, 68, 58, 0.1)';
|
||||
el.style.color = 'var(--rose)';
|
||||
el.innerHTML = `<strong>Blocked</strong> — ${result.reason}` +
|
||||
(result.matched_rule ? `<br>Rule: <code>${result.matched_rule}</code>` : '') +
|
||||
` <button class="btn-delete" onclick="allowDomain('${domain}')" style="color:var(--emerald);font-size:0.7rem;margin-left:0.4rem;">allow</button>`;
|
||||
} else {
|
||||
el.style.background = 'rgba(82, 122, 82, 0.1)';
|
||||
el.style.color = 'var(--emerald)';
|
||||
el.innerHTML = `<strong>Allowed</strong> — ${result.reason}` +
|
||||
(result.matched_rule ? `<br>Rule: <code>${result.matched_rule}</code>` : '');
|
||||
}
|
||||
} catch (err) {
|
||||
el.style.display = 'block';
|
||||
el.style.background = 'rgba(181, 68, 58, 0.1)';
|
||||
el.style.color = 'var(--rose)';
|
||||
el.textContent = 'Error: ' + err.message;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initial load + polling
|
||||
refresh();
|
||||
setInterval(refresh, 2000);
|
||||
|
||||
@@ -37,6 +37,7 @@ pub fn router(ctx: Arc<ServerCtx>) -> Router {
|
||||
.route("/blocking/pause", post(blocking_pause))
|
||||
.route("/blocking/allowlist", get(blocking_allowlist))
|
||||
.route("/blocking/allowlist", post(blocking_allowlist_add))
|
||||
.route("/blocking/check/{domain}", get(blocking_check))
|
||||
.route(
|
||||
"/blocking/allowlist/{domain}",
|
||||
delete(blocking_allowlist_remove),
|
||||
@@ -532,6 +533,14 @@ async fn blocking_pause(
|
||||
Json(serde_json::json!({ "paused_minutes": req.minutes }))
|
||||
}
|
||||
|
||||
async fn blocking_check(
|
||||
State(ctx): State<Arc<ServerCtx>>,
|
||||
Path(domain): Path<String>,
|
||||
) -> Json<crate::blocklist::BlockCheckResult> {
|
||||
let result = ctx.blocklist.lock().unwrap().check(&domain);
|
||||
Json(result)
|
||||
}
|
||||
|
||||
async fn blocking_allowlist(State(ctx): State<Arc<ServerCtx>>) -> Json<Vec<String>> {
|
||||
let list = ctx.blocklist.lock().unwrap().allowlist();
|
||||
Json(list)
|
||||
|
||||
@@ -12,6 +12,44 @@ pub struct BlocklistStore {
|
||||
last_refresh: Option<Instant>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct BlockCheckResult {
|
||||
pub blocked: bool,
|
||||
pub reason: String,
|
||||
pub matched_rule: Option<String>,
|
||||
}
|
||||
|
||||
impl BlockCheckResult {
|
||||
fn blocked(rule: &str, reason: &str) -> Self {
|
||||
Self {
|
||||
blocked: true,
|
||||
reason: reason.to_string(),
|
||||
matched_rule: Some(rule.to_string()),
|
||||
}
|
||||
}
|
||||
fn allowed(rule: &str, reason: &str) -> Self {
|
||||
Self {
|
||||
blocked: false,
|
||||
reason: reason.to_string(),
|
||||
matched_rule: Some(rule.to_string()),
|
||||
}
|
||||
}
|
||||
fn not_blocked() -> Self {
|
||||
Self {
|
||||
blocked: false,
|
||||
reason: "not in blocklist".to_string(),
|
||||
matched_rule: None,
|
||||
}
|
||||
}
|
||||
fn disabled() -> Self {
|
||||
Self {
|
||||
blocked: false,
|
||||
reason: "blocking is disabled".to_string(),
|
||||
matched_rule: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BlocklistStats {
|
||||
pub enabled: bool,
|
||||
pub paused: bool,
|
||||
@@ -73,6 +111,36 @@ impl BlocklistStore {
|
||||
false
|
||||
}
|
||||
|
||||
/// Check if a domain is blocked and return the reason.
|
||||
pub fn check(&self, domain: &str) -> BlockCheckResult {
|
||||
let domain = domain.to_lowercase();
|
||||
|
||||
if !self.enabled {
|
||||
return BlockCheckResult::disabled();
|
||||
}
|
||||
|
||||
if self.allowlist.contains(&domain) {
|
||||
return BlockCheckResult::allowed(&domain, "exact match in allowlist");
|
||||
}
|
||||
|
||||
if self.domains.contains(&domain) {
|
||||
return BlockCheckResult::blocked(&domain, "exact match in blocklist");
|
||||
}
|
||||
|
||||
let mut d = domain.as_str();
|
||||
while let Some(dot) = d.find('.') {
|
||||
d = &d[dot + 1..];
|
||||
if self.allowlist.contains(d) {
|
||||
return BlockCheckResult::allowed(d, "parent domain in allowlist");
|
||||
}
|
||||
if self.domains.contains(d) {
|
||||
return BlockCheckResult::blocked(d, "parent domain in blocklist");
|
||||
}
|
||||
}
|
||||
|
||||
BlockCheckResult::not_blocked()
|
||||
}
|
||||
|
||||
/// Atomically swap in a new domain set. Build the set outside the lock,
|
||||
/// then call this to swap — keeps lock hold time sub-microsecond.
|
||||
pub fn swap_domains(&mut self, domains: HashSet<String>, sources: Vec<String>) {
|
||||
|
||||
Reference in New Issue
Block a user