Files
pleiades/templates/quality.html

178 lines
8.5 KiB
HTML

{% extends "base.html" %}
{% block title %}IP Quality Tester — Clean Proxys{% endblock %}
{% block body %}
<style>
.qc { max-width: 720px; margin: 0 auto; }
.stage { display: flex; flex-direction: column; align-items: center; padding: 30px 0 20px; min-height: 320px; }
/* ---- 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" id="runbtn" value="Scan proxy ({{ price }} sats)">
</form>
<div class="stage" id="stage">
<div class="muted">Scan output appears here.</div>
</div>
</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 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){ 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){
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;
}
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 %}