Snapshot: full project state

This commit is contained in:
2026-10-06 23:43:40 -07:00
commit 24a36eec20
7 changed files with 701 additions and 0 deletions

289
static/index.html Normal file
View File

@@ -0,0 +1,289 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Payment Recon — Search Engine</title>
<style>
:root{--bg:#06060e;--card:#0e0e1a;--accent:#f7931a;--accent2:#00d4aa;--accent3:#6c63ff;--text:#e0e0e0;--muted:#777;--danger:#ff4757;--success:#2ed573;--warning:#ffa502;--radius:12px}
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--text);min-height:100vh;overflow-x:hidden}
body::before{content:'';position:fixed;top:-50%;left:-50%;width:200%;height:200%;background:radial-gradient(circle at 30% 20%,rgba(108,99,255,0.03) 0%,transparent 50%),radial-gradient(circle at 70% 80%,rgba(247,147,26,0.03) 0%,transparent 50%);pointer-events:none;z-index:0}
.app{position:relative;z-index:1;max-width:1400px;margin:0 auto;padding:1rem}
/* Header */
header{text-align:center;padding:2rem 1rem 1.5rem}
header h1{font-size:2rem;font-weight:800;background:linear-gradient(135deg,var(--accent),var(--accent2),var(--accent3));-webkit-background-clip:text;-webkit-text-fill-color:transparent;letter-spacing:-0.5px}
header p{color:var(--muted);font-size:0.85rem;margin-top:0.3rem}
/* Search Bar */
.search-bar{background:var(--card);border-radius:var(--radius);padding:0.7rem;display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;border:1px solid rgba(255,255,255,0.06);margin-bottom:1.5rem;position:sticky;top:1rem;z-index:10;backdrop-filter:blur(10px)}
.search-bar select,.search-bar button{background:rgba(0,0,0,0.4);color:var(--text);border:1px solid rgba(255,255,255,0.1);border-radius:6px;padding:0.5rem 0.8rem;font-size:0.78rem;cursor:pointer}
.search-bar select{min-width:150px}
.search-bar select:hover,.search-bar button:hover{border-color:var(--accent)}
.search-bar .btn-search{background:linear-gradient(135deg,var(--accent),#e6730d);color:#000;border:none;font-weight:700;padding:0.5rem 1.5rem;font-size:0.82rem}
.search-bar .btn-search:hover{filter:brightness(1.15)}
.search-bar .btn-search:disabled{opacity:0.5;cursor:not-allowed}
.search-bar .btn-scan{background:linear-gradient(135deg,var(--accent3),#8b5cf6);color:#fff;border:none;font-weight:700;padding:0.5rem 1.2rem;font-size:0.82rem}
/* Stats Bar */
.stats-bar{display:flex;gap:1rem;flex-wrap:wrap;margin-bottom:1rem;font-size:0.75rem;color:var(--muted)}
.stats-bar strong{color:var(--accent2)}
.stat-pill{background:var(--card);padding:0.3rem 0.7rem;border-radius:20px;border:1px solid rgba(255,255,255,0.05);font-size:0.68rem}
/* Results */
.results-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:0.8rem}
.site-card{background:var(--card);border-radius:var(--radius);overflow:hidden;border:1px solid rgba(255,255,255,0.05);transition:all 0.25s;display:flex;flex-direction:column}
.site-card:hover{transform:translateY(-2px);border-color:rgba(255,255,255,0.12);box-shadow:0 8px 30px rgba(0,0,0,0.3)}
.site-card .screenshot{height:180px;background:rgba(0,0,0,0.4);display:flex;align-items:center;justify-content:center;overflow:hidden;position:relative}
.site-card .screenshot img{width:100%;height:100%;object-fit:cover}
.site-card .screenshot .placeholder{font-size:2.5rem;opacity:0.3}
.site-card .body{padding:0.9rem;flex:1;display:flex;flex-direction:column}
.site-card .domain{font-weight:700;font-size:0.9rem;color:var(--accent2);margin-bottom:0.15rem}
.site-card .domain a{color:inherit;text-decoration:none}
.site-card .domain a:hover{text-decoration:underline}
.site-card .desc{font-size:0.72rem;color:var(--muted);line-height:1.4;margin:0.4rem 0;flex:1}
.site-card .tags{margin-top:0.4rem;display:flex;gap:0.3rem;flex-wrap:wrap}
.tag{display:inline-block;padding:0.15rem 0.45rem;border-radius:3px;font-size:0.6rem;font-weight:600}
.t-type{background:rgba(108,99,255,0.2);color:#a78bfa}
.t-industry{background:rgba(247,147,26,0.2);color:var(--accent)}
.t-country{background:rgba(0,212,170,0.15);color:var(--accent2);font-size:0.55rem}
.t-US{background:rgba(59,130,246,0.2);color:#60a5fa}
.t-EU{background:rgba(168,85,247,0.2);color:#a78bfa}
.t-INTL{background:rgba(251,191,36,0.2);color:#fbbf24}
.t-gambling{background:rgba(255,71,87,0.2);color:var(--danger)}
.t-ecommerce{background:rgba(0,212,170,0.2);color:var(--accent2)}
.t-adult{background:rgba(247,147,26,0.2);color:var(--accent)}
.t-electronics{background:rgba(59,130,246,0.2);color:#60a5fa}
.site-card .actions{margin-top:0.5rem;display:flex;gap:0.3rem}
.btn-sm{padding:0.25rem 0.6rem;border-radius:4px;border:none;cursor:pointer;font-size:0.65rem;font-weight:600}
.btn-visit{background:var(--accent2);color:#000}
.btn-visit:hover{filter:brightness(1.2)}
.btn-screenshot{background:rgba(108,99,255,0.2);color:#a78bfa;border:1px solid rgba(108,99,255,0.3)}
.btn-screenshot:hover{background:rgba(108,99,255,0.3)}
/* Loader */
.loader{text-align:center;padding:2rem}
.spinner{display:inline-block;width:30px;height:30px;border:3px solid rgba(255,255,255,0.1);border-top-color:var(--accent);border-radius:50%;animation:spin 0.8s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
/* Empty */
.empty{text-align:center;padding:3rem;color:var(--muted)}
.empty .icon{font-size:4rem;margin-bottom:1rem;opacity:0.3}
/* Responsive */
@media(max-width:768px){.results-grid{grid-template-columns:1fr}.search-bar{flex-direction:column;align-items:stretch}}
</style>
</head>
<body>
<div class="app">
<header>
<h1>🔍 Payment Recon</h1>
<p>Search the web for payment processors. Screenshots. AI descriptions. Organized by industry.</p>
</header>
<div class="search-bar">
<select id="processor-select">
<option value="">🎯 Select Processor</option>
</select>
<select id="industry-select">
<option value="">🏷️ All Industries</option>
</select>
<select id="country-select">
<option value="US" selected>🇺🇸 US Only</option>
<option value="">🌍 All Countries</option>
<option value="EU">🇪🇺 Europe</option>
<option value="INTL">🌍 International</option>
</select>
<button class="btn-search" onclick="doSearch()" id="btn-search">🔍 Search</button>
<button class="btn-scan" onclick="scanAll()" id="btn-scan">⚡ Scan All Processors</button>
<span style="font-size:0.68rem;color:var(--muted);margin-left:auto;" id="status-text">Ready</span>
</div>
<div class="stats-bar" id="stats-bar"></div>
<div class="results-grid" id="results"></div>
<div class="loader" id="loader" style="display:none"><div class="spinner"></div><p style="margin-top:0.5rem;font-size:0.75rem;color:var(--muted);">Searching...</p></div>
<div id="screenshot-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.85);z-index:100;align-items:center;justify-content:center;" onclick="this.style.display='none'">
<img id="screenshot-img" style="max-width:90vw;max-height:90vh;border-radius:8px;box-shadow:0 20px 60px rgba(0,0,0,0.5);">
</div>
</div>
<script>
let allProcessors = {};
let allIndustries = [];
let currentResults = [];
function safeId(str){return 'id-'+btoa(unescape(encodeURIComponent(str))).replace(/[+/=]/g,'')}
async function init(){
const r = await fetch('/api/processors');
const data = await r.json();
allProcessors = data.processors;
// Populate processor dropdown
const ps = document.getElementById('processor-select');
for(const [name, info] of Object.entries(allProcessors)){
ps.innerHTML += `<option value="${name}">${name} (${info.type})</option>`;
}
// Populate industry dropdown
allIndustries = data.industries;
const is = document.getElementById('industry-select');
for(const ind of allIndustries){
is.innerHTML += `<option value="${ind}">${ind}</option>`;
}
document.getElementById('stats-bar').innerHTML = `
<span class="stat-pill"><strong>${Object.keys(allProcessors).length}</strong> processors</span>
<span class="stat-pill"><strong>${allIndustries.length}</strong> industries</span>
`;
}
async function doSearch(){
const proc = document.getElementById('processor-select').value;
if(!proc) return alert('Select a processor first');
const industry = document.getElementById('industry-select').value;
const country = document.getElementById('country-select').value;
showLoader(true);
document.getElementById('status-text').textContent = `Searching for ${proc}...`;
const url = `/api/search/${encodeURIComponent(proc)}?industry=${encodeURIComponent(industry)}&country=${encodeURIComponent(country)}&max=12`;
const r = await fetch(url);
const data = await r.json();
currentResults = data.results || [];
renderResults(currentResults, proc);
showLoader(false);
document.getElementById('status-text').textContent = `${data.result_count} results for ${proc}`;
}
async function scanAll(){
if(!confirm('Scan all processors? This may take a minute.')) return;
showLoader(true);
const industry = document.getElementById('industry-select').value;
document.getElementById('status-text').textContent = 'Scanning all processors...';
const url = `/api/search-all?industry=${encodeURIComponent(industry)}&country=${encodeURIComponent(country)}`;
const r = await fetch(url);
const data = await r.json();
let all = [];
for(const [name, result] of Object.entries(data)){
if(result.results){
all = all.concat(result.results);
}
}
currentResults = all;
renderResults(currentResults, 'All Processors');
showLoader(false);
document.getElementById('status-text').textContent = `${all.length} results across all processors`;
}
function renderResults(results, procName){
const grid = document.getElementById('results');
if(!results.length){
grid.innerHTML = `<div class="empty"><div class="icon">🔍</div><p>No results found for ${procName}.</p><p style="font-size:0.7rem;margin-top:0.2rem;">Try a different processor or industry.</p></div>`;
return;
}
grid.innerHTML = results.map(s => {
const domain = s.domain || (()=>{try{return new URL(s.url||'').hostname.replace('www.','')}catch{return s.domain||'unknown'}})();
const proc = s.processor || procName;
const tags = [
`<span class="tag t-type">${s.processor_type||''}</span>`,
`<span class="tag t-industry t-${s.industry||'ecommerce'}">${s.industry||'ecommerce'}</span>`,
`<span class="tag t-country t-${s.country||'US'}">${s.country||'US'}</span>`
].join('');
return `
<div class="site-card">
<div class="screenshot" id="ss-${safeId(domain)}">
<span class="placeholder">🌐</span>
</div>
<div class="body">
<div class="domain"><a href="${s.url||'https://'+domain}" target="_blank">${domain}</a></div>
<div class="desc">${s.snippet||s.description||'Visit site for details.'}</div>
<div class="tags">${tags}</div>
<div class="actions">
<button class="btn-sm btn-visit" onclick="window.open('${s.url||'https://'+domain}','_blank')">Visit →</button>
<button class="btn-sm btn-screenshot" onclick="takeScreenshot('${domain}','${s.url||'https://'+domain}','${safeId(domain)}')">📸 Screenshot</button>
</div>
</div>
</div>
`;
}).join('');
document.getElementById('stats-bar').innerHTML = `
<span class="stat-pill"><strong>${results.length}</strong> results</span>
<span class="stat-pill">Processor: <strong>${procName}</strong></span>
${[...new Set(results.map(r=>r.industry))].map(i=>`<span class="stat-pill">${i}</span>`).join('')}
`;
}
async function takeScreenshot(domain, url, id){
const el = document.getElementById('ss-'+id);
el.innerHTML = '<div class="spinner" style="width:20px;height:20px;"></div>';
try{
// Queue analysis job
const r = await fetch('/api/analyze', {
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({sites:[{domain,url}],processor:currentResults[0]?.processor||'Unknown'})
});
const {job_id} = await r.json();
// Poll for result
let attempts=0;
while(attempts<30){
await new Promise(r=>setTimeout(r,1000));
const jr = await fetch(`/api/job/${job_id}`);
const job = await jr.json();
if(job.status==='complete'){
const result = job.results[0];
if(result?.description){
// Update description
for(const card of document.querySelectorAll('.site-card')){
if(card.querySelector('.domain a')?.textContent === domain){
card.querySelector('.desc').textContent = result.description;
}
}
}
if(result?.screenshot_base64){
el.innerHTML = `<img src="data:image/png;base64,${result.screenshot_base64}" style="width:100%;height:100%;object-fit:cover" onclick="event.stopPropagation();showFullScreenshot('${result.screenshot_base64}')">`;
} else {
el.innerHTML = '<span class="placeholder">📸</span>';
}
break;
}
attempts++;
}
if(attempts>=30) el.innerHTML = '<span class="placeholder">⏰</span>';
}catch(e){
el.innerHTML = '<span class="placeholder">❌</span>';
console.error(e);
}
}
function showLoader(show){document.getElementById('loader').style.display=show?'block':'none'}
function showFullScreenshot(b64){
const modal = document.getElementById('screenshot-modal');
const img = document.getElementById('screenshot-img');
img.src = `data:image/png;base64,${b64}`;
modal.style.display = 'flex';
}
init();
</script>
<div style="text-align:center;padding:1rem;margin-top:1.5rem;border-top:1px solid rgba(255,255,255,0.05)">
<a href="https://buymeacoffee.com/r26xrthzttg" target="_blank" rel="noopener" style="display:inline-flex;align-items:center;gap:0.5rem;background:linear-gradient(135deg,#FF813F,#FF5E0E);color:#fff;padding:0.45rem 1.1rem;border-radius:25px;text-decoration:none;font-weight:600;font-size:0.75rem;transition:all 0.2s;box-shadow:0 3px 12px rgba(255,94,14,0.25)" onmouseover="this.style.transform='scale(1.04)';this.style.boxShadow='0 5px 18px rgba(255,94,14,0.4)'" onmouseout="this.style.transform='scale(1)';this.style.boxShadow='0 3px 12px rgba(255,94,14,0.25)'">
<span style="font-size:1rem">☕</span> Support This Project — Buy Me a Coffee
</a>
</div>
</body>
</html>