feat: add Validate Selected button, Alpha only and No SLD filters to beauty Browse

- /api/validate/batch endpoint: HTTP-check only (no DeepSeek), accepts up to 500 domains
- Validate Selected bulk button: runs validate in 500-domain chunks, shows live/dead summary
- Alpha only checkbox: passes alpha_only=true to /api/domains to exclude hyphens/numbers
- No SLD checkbox: passes no_sld=true to /api/domains to skip com.es / co.uk style domains
- Both flags wired into loadDomains() and resetFilters()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-05 07:59:32 +02:00
parent 5672b61b5e
commit 7ec0304dea
2 changed files with 59 additions and 5 deletions

View File

@@ -188,6 +188,26 @@ async def validator_status():
# ── Pre-screen (shared) ───────────────────────────────────────────────────────
@app.post("/api/validate/batch")
async def validate_batch(body: dict):
"""HTTP-check only — no DeepSeek classification. Fast live/dead check for bulk selection."""
domains_list = body.get("domains", [])
if not domains_list:
return JSONResponse({"error": "no domains provided"}, status_code=400)
if len(domains_list) > 500:
return JSONResponse({"error": "max 500 per batch"}, status_code=400)
from app.prescreener import prescreen_domains
results = await prescreen_domains(domains_list)
await save_prescreen_results(results)
counts: dict = {}
for r in results:
s = r.get("prescreen_status", "dead")
counts[s] = counts.get(s, 0) + 1
return {"total": len(domains_list), "live": counts.get("live", 0),
"dead": counts.get("dead", 0), "parked": counts.get("parked", 0),
"redirect": counts.get("redirect", 0), "error": counts.get("error", 0)}
@app.post("/api/prescreen/batch")
async def prescreen_batch(body: dict):
domains_list = body.get("domains", [])

View File

@@ -145,6 +145,12 @@ textarea{width:100%;resize:vertical;font-family:monospace;font-size:12px}
<option value="landing_page">Landing Page</option>
</select>
<input x-model="f.country" placeholder="Country (ES, FR…)" style="width:100px" @keyup.enter="goSearch()">
<label class="tog" style="font-size:12px;display:flex;align-items:center;gap:4px;cursor:pointer;white-space:nowrap">
<input type="checkbox" x-model="f.alpha_only" @change="goSearch()"><span>Alpha only</span>
</label>
<label class="tog" style="font-size:12px;display:flex;align-items:center;gap:4px;cursor:pointer;white-space:nowrap">
<input type="checkbox" x-model="f.no_sld" @change="goSearch()"><span>No SLD</span>
</label>
<select x-model="f.limit" @change="goSearch()">
<option value="50">50</option>
<option value="100" selected>100</option>
@@ -159,6 +165,10 @@ textarea{width:100%;resize:vertical;font-family:monospace;font-size:12px}
<!-- Bulk bar (visible when items selected) -->
<div class="bulk-bar" x-show="selected.length>0">
<span style="color:var(--accent);font-weight:600;font-size:12px" x-text="selected.length+' selected'"></span>
<button class="btn-secondary btn-sm" @click="validateSelected()" :disabled="validating">
<span x-show="!validating">Validate Selected</span>
<span x-show="validating">Validating…</span>
</button>
<button class="btn-ok btn-sm" @click="prescreenSelected()" :disabled="prescreening">
<span x-show="!prescreening">Pre-screen Selected</span>
<span x-show="prescreening">Screening…</span>
@@ -430,10 +440,10 @@ function app() {
valSt: {running:false,processed:0,live:0,dead:0,error:0,parked:0,redirect:0,skipped:0,offset:0,rate:0},
valTld: '', valRescan: false,
toasts: [],
prescreening: false,
prescreening: false, validating: false,
exportQuality: '', exportCountry: '',
f: {keyword:'', tld:'', prescreen_status:'live', niche:'beauty_cosmetics',
site_type:'ecommerce', country:'', limit:'100', page:1},
site_type:'ecommerce', country:'', alpha_only:false, no_sld:false, limit:'100', page:1},
pf: {quality:'', country:'', limit:'100', page:1},
async init() {
@@ -478,8 +488,10 @@ function app() {
this.loading = true;
try {
const p = new URLSearchParams({page: this.f.page, limit: this.f.limit});
if (this.f.keyword) p.set('keyword', this.f.keyword.trim());
if (this.f.tld) p.set('tld', this.f.tld.trim());
if (this.f.keyword) p.set('keyword', this.f.keyword.trim());
if (this.f.tld) p.set('tld', this.f.tld.trim());
if (this.f.alpha_only) p.set('alpha_only', 'true');
if (this.f.no_sld) p.set('no_sld', 'true');
const d = await fetch('/api/domains?' + p).then(r=>r.json());
this.domainsTotal = d.total || 0;
let rows = d.results || [];
@@ -509,7 +521,7 @@ function app() {
resetFilters() {
this.f = {keyword:'', tld:'', prescreen_status:'live', niche:'beauty_cosmetics',
site_type:'ecommerce', country:'', limit:'100', page:1};
site_type:'ecommerce', country:'', alpha_only:false, no_sld:false, limit:'100', page:1};
this.selected = [];
this.loadDomains();
},
@@ -518,6 +530,28 @@ function app() {
this.selected = e.target.checked ? this.domains.map(r=>r.domain) : [];
},
async validateSelected() {
if (!this.selected.length || this.validating) return;
this.validating = true;
this.notify(`Validating ${this.selected.length} domains…`, 'info');
try {
const chunks = [];
for (let i=0; i<this.selected.length; i+=500) chunks.push(this.selected.slice(i,i+500));
let totals = {live:0, dead:0, parked:0, redirect:0, error:0};
for (const chunk of chunks) {
const d = await fetch('/api/validate/batch', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({domains: chunk}),
}).then(r=>r.json());
for (const k of Object.keys(totals)) totals[k] += d[k]||0;
}
this.notify(`${totals.live} live · ☠ ${totals.dead} dead · 🅿 ${totals.parked} parked · ↗ ${totals.redirect} redirect`, 'success');
this.selected = [];
await this.loadDomains();
} catch(e) { this.notify('Validate failed: '+e.message, 'error'); }
finally { this.validating = false; }
},
async prescreenSelected() {
if (!this.selected.length || this.prescreening) return;
this.prescreening = true;