Interactive quality tester: radar sweep + cycling scan status + animated score gauge + staggered check reveal
This commit is contained in:
@@ -1,53 +1,177 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}IP Quality Tester — Clean Proxys{% endblock %}
|
||||
{% block body %}
|
||||
<h2>IP Quality <span class="accent">Tester</span></h2>
|
||||
<p class="sub">Check how clean a proxy is before you trust it. {{ price }} sats (~$0.15) per check, billed to your wallet.</p>
|
||||
<style>
|
||||
.qc { max-width: 720px; margin: 0 auto; }
|
||||
.stage { display: flex; flex-direction: column; align-items: center; padding: 30px 0 20px; min-height: 320px; }
|
||||
|
||||
<div class="card">
|
||||
<h3>Run a check</h3>
|
||||
<p class="muted" style="font-size:14px">Leave blank to test your own Clean Proxys proxy, or paste any <span class="mono">socks5://</span> or <span class="mono">http://</span> proxy.</p>
|
||||
/* ---- radar (scanning state) ---- */
|
||||
.radar {
|
||||
position: relative; width: 220px; height: 220px; border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(0,229,255,.04) 0%, rgba(3,6,11,.6) 70%);
|
||||
border: 1px solid var(--border); overflow: hidden;
|
||||
box-shadow: 0 0 50px rgba(0,229,255,.12), inset 0 0 40px rgba(0,229,255,.05);
|
||||
}
|
||||
.radar .ring { position: absolute; border-radius: 50%; border: 1px solid rgba(0,229,255,.18); }
|
||||
.radar .r1 { inset: 15%; } .radar .r2 { inset: 30%; } .radar .r3 { inset: 45%; }
|
||||
.radar .cross::before, .radar .cross::after {
|
||||
content:""; position:absolute; background: rgba(0,229,255,.15); left: 50%; top: 0; height: 100%; width: 1px;
|
||||
}
|
||||
.radar .cross::after { transform: rotate(90deg); }
|
||||
.radar .sweep {
|
||||
position: absolute; inset: 0; border-radius: 50%;
|
||||
background: conic-gradient(from 0deg, rgba(0,255,156,.55), rgba(0,255,156,.12) 22%, transparent 40%);
|
||||
animation: sweep 2.4s linear infinite;
|
||||
}
|
||||
@keyframes sweep { to { transform: rotate(360deg); } }
|
||||
.radar .dot {
|
||||
position: absolute; left: 50%; top: 50%; width: 8px; height: 8px; margin: -4px;
|
||||
border-radius: 50%; background: var(--neon); box-shadow: 0 0 12px var(--neon);
|
||||
animation: pulse 1.2s ease-in-out infinite;
|
||||
}
|
||||
.radar .blip {
|
||||
position: absolute; width: 6px; height: 6px; border-radius: 50%; background: var(--neon2);
|
||||
box-shadow: 0 0 10px var(--neon2); opacity: 0;
|
||||
}
|
||||
@keyframes pulse { 0%,100% { transform: scale(.7); opacity: .6; } 50% { transform: scale(1.3); opacity: 1; } }
|
||||
.scan-status {
|
||||
margin-top: 22px; font-family: var(--mono); font-size: 14px; color: var(--neon2);
|
||||
letter-spacing: .5px; min-height: 20px;
|
||||
}
|
||||
.scan-status::after { content: "▌"; animation: blink 1s step-end infinite; }
|
||||
@keyframes blink { 50% { opacity: 0; } }
|
||||
|
||||
/* ---- gauge (result state) ---- */
|
||||
.gauge-wrap { position: relative; width: 220px; height: 220px; }
|
||||
.gauge-wrap svg { transform: rotate(-90deg); }
|
||||
.gauge-wrap .track { fill: none; stroke: rgba(255,255,255,.08); stroke-width: 14; }
|
||||
.gauge-wrap .fill { fill: none; stroke-width: 14; stroke-linecap: round; transition: stroke-dashoffset 1.4s cubic-bezier(.22,1,.36,1); }
|
||||
.gauge-wrap .val {
|
||||
position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
}
|
||||
.gauge-wrap .val .num { font-family: var(--mono); font-size: 56px; font-weight: 800; line-height: 1; }
|
||||
.gauge-wrap .val .sub { font-size: 11px; letter-spacing: 2px; color: var(--muted); text-transform: uppercase; margin-top: 4px; }
|
||||
|
||||
.checks { width: 100%; margin-top: 10px; }
|
||||
.chk {
|
||||
display: flex; align-items: center; gap: 12px; padding: 10px 14px; border-radius: 10px;
|
||||
border: 1px solid var(--border); margin: 8px 0; background: var(--surface);
|
||||
opacity: 0; transform: translateY(8px); animation: in .4s ease forwards;
|
||||
}
|
||||
@keyframes in { to { opacity: 1; transform: translateY(0); } }
|
||||
.chk .ic { font-size: 18px; width: 24px; text-align: center; }
|
||||
.chk .nm { font-weight: 600; flex: 1; }
|
||||
.chk .dt { color: var(--muted); font-size: 13px; text-align: right; }
|
||||
.chk.pass { border-color: rgba(0,255,156,.25); }
|
||||
.chk.fail { border-color: rgba(255,77,109,.25); }
|
||||
.verdict { text-align: center; margin-top: 14px; font-weight: 700; letter-spacing: 1px; }
|
||||
</style>
|
||||
|
||||
<h2>IP Quality <span class="accent">Tester</span></h2>
|
||||
<p class="sub" style="text-align:center">Check how clean a proxy is before you trust it. {{ price }} sats (~$0.15) per scan, billed to your wallet.</p>
|
||||
|
||||
<div class="card qc">
|
||||
<h3>Run a scan</h3>
|
||||
<p class="muted" style="font-size:14px">Leave blank to test your own Clean Proxys proxy, or paste any <span class="mono">socks5://</span> / <span class="mono">http://</span> proxy.</p>
|
||||
<form id="qf" onsubmit="return runQuality(event)">
|
||||
<label>Proxy (optional)</label>
|
||||
<input type="text" id="proxy" placeholder="socks5://user:pass@host:1080" style="font-family:var(--mono)">
|
||||
<input type="submit" value="Run quality check ({{ price }} sats)">
|
||||
<input type="submit" id="runbtn" value="Scan proxy ({{ price }} sats)">
|
||||
</form>
|
||||
|
||||
<div class="stage" id="stage">
|
||||
<div class="muted">Scan output appears here.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="result" style="margin-top:18px"></div>
|
||||
|
||||
<script>
|
||||
const STATUSES = ["Resolving egress IP…", "Classifying ISP / ASN…", "Checking proxy & VPN flags…", "Querying 3 blacklists…", "Measuring latency…", "Testing header anonymity…"];
|
||||
const C = 2 * Math.PI * 88; // gauge circumference (r=88)
|
||||
|
||||
function radarHTML(){
|
||||
return '<div class="radar"><div class="cross"></div><div class="ring r1"></div><div class="ring r2"></div><div class="ring r3"></div><div class="sweep"></div><div class="dot"></div><div class="blip" id="blip"></div></div><div class="scan-status" id="status">Booting scanner…</div>';
|
||||
}
|
||||
|
||||
async function runQuality(e){
|
||||
e.preventDefault();
|
||||
const r = document.getElementById('result');
|
||||
r.innerHTML = '<div class="card"><p class="muted">Running checks…</p></div>';
|
||||
const stage = document.getElementById('stage');
|
||||
const btn = document.getElementById('runbtn');
|
||||
btn.disabled = true; btn.value = 'Scanning…';
|
||||
stage.innerHTML = radarHTML();
|
||||
|
||||
// cycling status + radar blips
|
||||
let i = 0;
|
||||
const status = document.getElementById('status');
|
||||
const blip = document.getElementById('blip');
|
||||
status.textContent = STATUSES[0];
|
||||
const iv = setInterval(() => {
|
||||
i = (i + 1) % STATUSES.length;
|
||||
status.textContent = STATUSES[i];
|
||||
// random blip position on the radar
|
||||
const a = Math.random() * Math.PI * 2, r = 20 + Math.random() * 75;
|
||||
blip.style.left = (110 + Math.cos(a) * r) + 'px';
|
||||
blip.style.top = (110 + Math.sin(a) * r) + 'px';
|
||||
blip.style.opacity = 1;
|
||||
setTimeout(() => blip.style.opacity = 0, 500);
|
||||
}, 700);
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('proxy', document.getElementById('proxy').value);
|
||||
let data;
|
||||
try { data = await (await fetch('/quality/run', {method:'POST', body: fd})).json(); }
|
||||
catch(err){ r.innerHTML = '<div class="flash warn">Check failed.</div>'; return false; }
|
||||
catch(err){ clearInterval(iv); stage.innerHTML = '<div class="flash warn">Scan failed.</div>'; btn.disabled=false; btn.value='Scan proxy ({{ price }} sats)'; return false; }
|
||||
|
||||
clearInterval(iv);
|
||||
if (data.error){
|
||||
r.innerHTML = '<div class="flash warn">'+data.error+' — <a href="/wallet">deposit</a> first.</div>';
|
||||
stage.innerHTML = '<div class="flash warn">'+data.error+' — <a href="/wallet">deposit</a> first.</div>';
|
||||
btn.disabled=false; btn.value='Scan proxy ({{ price }} sats)';
|
||||
return false;
|
||||
}
|
||||
const b = data.breakdown || {};
|
||||
const checksHtml = (data.checks||[]).map(c =>
|
||||
'<tr><td>'+(c[1]?'✅':'❌')+'</td><td>'+c[0]+'</td><td class="muted" style="font-size:13px">'+c[2]+'</td></tr>'
|
||||
).join('');
|
||||
const scoreColor = data.score >= 70 ? 'var(--neon)' : (data.score >= 40 ? 'var(--warn)' : 'var(--danger)');
|
||||
const bl = (b.blacklists && b.blacklists.length) ? b.blacklists.join(', ') : 'none';
|
||||
r.innerHTML =
|
||||
'<div class="card">'
|
||||
+ '<div style="display:flex;align-items:center;gap:22px;flex-wrap:wrap">'
|
||||
+ '<div style="font-size:54px;font-weight:800;font-family:var(--mono);color:'+scoreColor+'">'+data.score+'<span style="font-size:18px;color:var(--muted)">/100</span></div>'
|
||||
+ '<div><div class="lbl" style="color:var(--muted)">'+ (b.geo||'') + ' · ' + (b.isp||'') + '</div>'
|
||||
+ '<div class="muted" style="font-size:14px">egress <span class="mono">'+(b.egress_ip||'-')+'</span> · '+ (b.latency_ms||'') +'ms</div>'
|
||||
+ '<div class="muted" style="font-size:13px">hosting: '+(b.hosting?'yes':'no')+' · proxy-flagged: '+(b.proxy_flagged?'yes':'no')+' · blacklists: '+bl+'</div></div>'
|
||||
+ '</div>'
|
||||
+ '<table style="width:100%;margin-top:18px"><tr><th></th><th>Check</th><th>Detail</th></tr>'+checksHtml+'</table>'
|
||||
+ '<p class="muted" style="font-size:12px;margin-top:12px">charged '+data.charged_sats+' sats</p>'
|
||||
+ '</div>';
|
||||
renderResult(data);
|
||||
btn.disabled=false; btn.value='Scan proxy ({{ price }} sats)';
|
||||
return false;
|
||||
}
|
||||
|
||||
function renderResult(data){
|
||||
const stage = document.getElementById('stage');
|
||||
const b = data.breakdown || {};
|
||||
const color = data.score >= 70 ? 'var(--neon)' : (data.score >= 40 ? 'var(--warn)' : 'var(--danger)');
|
||||
const verdict = data.score >= 70 ? 'CLEAN — ready to go' : (data.score >= 40 ? 'USABLE — check the flagged items' : 'DIRTY — not recommended');
|
||||
|
||||
stage.innerHTML =
|
||||
'<div class="gauge-wrap">'
|
||||
+ '<svg width="220" height="220"><circle class="track" cx="110" cy="110" r="88"></circle>'
|
||||
+ '<circle class="fill" id="fill" cx="110" cy="110" r="88" stroke="'+color+'" stroke-dasharray="'+C+'" stroke-dashoffset="'+C+'"></circle></svg>'
|
||||
+ '<div class="val"><div class="num" id="num" style="color:'+color+'">0</div><div class="sub">score / 100</div></div>'
|
||||
+ '</div>'
|
||||
+ '<div class="muted" style="margin-top:8px;font-size:14px">'+(b.geo||'')+' · '+(b.isp||'')+'</div>'
|
||||
+ '<div class="muted" style="font-size:13px">egress <span class="mono">'+(b.egress_ip||'-')+'</span> · '+ (b.latency_ms||'') +'ms</div>'
|
||||
+ '<div class="checks" id="checks"></div>'
|
||||
+ '<div class="verdict" style="color:'+color+'">'+verdict+'</div>';
|
||||
|
||||
// animate gauge + count-up
|
||||
const fill = document.getElementById('fill');
|
||||
const num = document.getElementById('num');
|
||||
const target = data.score, dur = 1300, t0 = performance.now();
|
||||
function tick(t){
|
||||
const p = Math.min(1, (t - t0) / dur);
|
||||
const ease = 1 - Math.pow(1 - p, 3); // cubic ease-out
|
||||
const val = Math.round(target * ease);
|
||||
num.textContent = val;
|
||||
fill.style.strokeDashoffset = (C * (1 - target/100 * ease)).toFixed(2);
|
||||
if (p < 1) requestAnimationFrame(tick);
|
||||
}
|
||||
requestAnimationFrame(tick);
|
||||
|
||||
// staggered check reveal
|
||||
const chkWrap = document.getElementById('checks');
|
||||
(data.checks || []).forEach((c, idx) => {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'chk ' + (c[1] ? 'pass' : 'fail');
|
||||
el.style.animationDelay = (0.5 + idx * 0.12) + 's';
|
||||
el.innerHTML = '<span class="ic">'+(c[1]?'✅':'❌')+'</span><span class="nm">'+c[0]+'</span><span class="dt">'+c[2]+'</span>';
|
||||
chkWrap.appendChild(el);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user