domainhunter-panel/index.html
Adrian Miesikowski b01d32067f
Some checks failed
CI / lint (push) Has been cancelled
Deploy / deploy (push) Has been cancelled
feat: add toggle to filter dictionary-based domain scans
2026-02-19 10:59:36 +01:00

345 lines
14 KiB
HTML

<!doctype html>
<html lang="pl">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Domain Hunter Panel</title>
<style>
body{font-family:Inter,Arial,sans-serif;background:#0b1020;color:#e8ecff;margin:0;padding:12px}
.wrap{max-width:1200px;margin:0 auto}
.card{background:#121933;border:1px solid #2a376b;border-radius:12px;padding:12px;margin-bottom:12px}
h1{margin:0 0 8px;font-size:24px} h2{margin:0 0 10px;font-size:17px}
.muted{color:#9aa6d1}
input,button,select{background:#0f1530;color:#e8ecff;border:1px solid #3a4a87;border-radius:8px;padding:8px}
.controls{display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end}
.controls label{display:flex;flex-direction:column;gap:6px;min-width:150px}
.grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.table-wrap{overflow:auto;-webkit-overflow-scrolling:touch}
table{width:100%;border-collapse:collapse;font-size:13px;min-width:760px}
th,td{border-bottom:1px solid #24315f;padding:8px;text-align:left;vertical-align:top}
th.sortable{cursor:pointer;user-select:none}
th.sortable:hover{background:#1a2550}
a{color:#9ec5ff;text-decoration:underline;text-underline-offset:2px}
a:hover{color:#cfe2ff}
td b{color:#ffffff;font-weight:700;letter-spacing:0.1px}
.pill{display:inline-flex;align-items:center;gap:6px;background:#1f2b56;border:1px solid #3a4a87;border-radius:999px;padding:4px 8px;margin:4px 6px 4px 0;font-size:12px}
.pill input{width:78px;padding:4px 6px}
.ok{color:#63db8f}.bad{color:#ff8b8b}
.chip{display:inline-block;padding:2px 8px;border-radius:999px;font-size:11px;background:#264a32;color:#9df5bc;border:1px solid #3f8b5b}
.tabbar{display:flex;gap:8px;flex-wrap:wrap;margin:8px 0 0}
.tabbtn{cursor:pointer}
.tabbtn.active{background:#2a3f86;border-color:#6e8cff;color:#fff}
.small{font-size:12px}
@media (max-width: 980px){.grid{grid-template-columns:1fr}}
@media (max-width: 700px){
body{padding:10px}
.controls{flex-direction:column;align-items:stretch}
.controls label{width:100%;min-width:0}
button{width:100%}
}
</style>
</head>
<body>
<div class="wrap">
<h1>Domain Hunter Panel</h1>
<p class="muted">Domeny z Mongo + ceny live per TLD pobierane dynamicznie z rejestratorów (z krótkim cache).</p>
<div class="card">
<h2>Filtr domen</h2>
<div class="tabbar">
<button id="tabCurrent" class="tabbtn active">Bieżący skan</button>
<button id="tabAll" class="tabbtn">Wszystkie rekordy</button>
</div>
<div class="controls">
<label>Szukaj <input id="q" placeholder="np. lead, ai, crm" /></label>
<label>TLD
<select id="tld"><option value="">Wszystkie</option><option>pl</option><option>com</option><option>ai</option></select>
</label>
<label>LLM decyzja
<select id="decisionFilter">
<option value="">Wszystkie</option>
<option value="buy_now">buy_now</option>
<option value="watch">watch</option>
<option value="skip">skip</option>
<option value="none">brak</option>
</select>
</label>
<label>Status
<select id="statusFilter">
<option value="">Wszystkie</option>
<option value="available">available</option>
<option value="unknown">unknown</option>
</select>
</label>
<label>Min score <input id="minScore" type="number" min="0" max="100" step="1" placeholder="np. 70" /></label>
<label class="small"><input type="checkbox" id="onlyDictionary" /> Tylko słownikowe</label>
<label>Sortuj po
<select id="sortBy">
<option value="score">score</option>
<option value="llm_score">llm_score</option>
<option value="domain">domena</option>
<option value="scanned_at">czas skanu</option>
</select>
</label>
<label>Kierunek
<select id="sortDir">
<option value="desc">malejąco</option>
<option value="asc">rosnąco</option>
</select>
</label>
<label class="small"><input type="checkbox" id="onlyNew" /> Tylko nowe domeny</label>
<label class="small"><input type="checkbox" id="autoRefresh" checked /> Auto-refresh 60s</label>
<button id="reload">Odśwież</button>
</div>
<p class="muted" id="meta"></p>
</div>
<div class="card">
<h2>TOP domen + porównanie cen (rejestracja / odnowienie)</h2>
<div class="table-wrap">
<table>
<thead><tr><th class="sortable" data-sort="domain">Domena</th><th class="sortable" data-sort="tld">TLD</th><th class="sortable" data-sort="score">Score</th><th class="sortable" data-sort="llm_score">LLM</th><th class="sortable" data-sort="status">Status</th><th>Run</th><th class="sortable" data-sort="scanned_at">Skan</th><th>Oferty (top 3)</th></tr></thead>
<tbody id="rows"></tbody>
</table>
</div>
</div>
</div>
<script>
let domains = [];
let visibleDomains = [];
let livePriceByTld = {};
let viewMode = 'current';
let metaBase = '';
function money(v){ return (v===null || v===undefined || Number.isNaN(Number(v))) ? '—' : `${Number(v).toFixed(2)}`; }
function domainCheckUrl(domain){
const d = encodeURIComponent(domain);
return `https://www.whois.com/whois/${d}`;
}
function registrarDomainUrl(name, domain){
const d = encodeURIComponent(domain);
const n = (name || '').toLowerCase();
if(n.includes('cyber')) return `https://cyberfolks.pl/domeny-rejestracja/?slowa=${d}`;
if(n.includes('home.pl')) return `https://home.pl/szukaj-domeny/#d:${d}`;
if(n.includes('nazwa')) return `https://www.nazwa.pl/domeny/wyniki-wyszukiwania-domen/?domains=${d}`;
if(n.includes('ovh')) return `https://order.eu.ovhcloud.com/pl/order/webcloud/?#/webCloud/domain/select?selection=~()&domain=${d}`;
if(n.includes('aftermarket')) return `https://aftermarket.pl/szukaj/${d}`;
if(n.includes('porkbun')) return `https://porkbun.com/checkout/search?q=${d}`;
return domainCheckUrl(domain);
}
async function loadLivePricesForDomains(domainsList){
const tlds = [...new Set((domainsList||[]).map(x => x.tld).filter(Boolean))];
const next = {};
for (const tld of tlds){
const u = new URL('./api.php', location.href);
u.searchParams.set('action','live_prices');
u.searchParams.set('tld', tld);
try {
const d = await (await fetch(u)).json();
next[tld] = (d.items || []).map(x => ({
registrar: x.registrar,
register: x.register_price,
renew: x.renew_price,
twoYear: (x.register_price !== null && x.renew_price !== null) ? (Number(x.register_price) + Number(x.renew_price)) : null,
checkedAt: x.checked_at || null,
live: true
}));
} catch(e){
next[tld] = [];
}
}
livePriceByTld = next;
}
function topOffersByTld(tld){
const offers = (livePriceByTld[tld] || []).slice();
offers.sort((a,b)=>{
const av = a.twoYear === null ? Number.POSITIVE_INFINITY : a.twoYear;
const bv = b.twoYear === null ? Number.POSITIVE_INFINITY : b.twoYear;
return av - bv;
});
return offers.slice(0,3);
}
async function loadDomains(){
const q = document.getElementById('q').value.trim();
const tld = document.getElementById('tld').value;
const onlyNew = document.getElementById('onlyNew').checked;
const u = new URL('./api.php', location.href);
if(viewMode === 'all'){
u.searchParams.set('action','all_domains');
u.searchParams.set('limit','2000');
} else {
u.searchParams.set('action','domains');
if(onlyNew) u.searchParams.set('only_new', '1');
}
if(q) u.searchParams.set('q', q);
if(tld) u.searchParams.set('tld', tld);
const d = await (await fetch(u)).json();
domains = d.domains || [];
if(viewMode === 'all'){
metaBase = `widok: wszystkie rekordy | limit: ${d.meta?.limit||'—'} | widoczne: ${domains.length}`;
} else {
metaBase = `runId: ${d.meta?.runId||'—'} | prev: ${d.meta?.prevRunId||'—'} | scannedAt: ${d.meta?.scannedAt||'—'} | nowe: ${d.meta?.newCount||0} | widoczne: ${domains.length}`;
}
document.getElementById('meta').textContent = metaBase;
}
function applyFiltersAndSort(){
const decision = document.getElementById('decisionFilter').value;
const status = document.getElementById('statusFilter').value;
const minScoreRaw = document.getElementById('minScore').value;
const minScore = minScoreRaw === '' ? null : Number(minScoreRaw);
const sortBy = document.getElementById('sortBy').value;
const sortDir = document.getElementById('sortDir').value;
const onlyDictionary = document.getElementById('onlyDictionary').checked;
let arr = (domains || []).filter(d => {
if (decision === 'none' && d.decision) return false;
if (decision && decision !== 'none' && (d.decision || '') !== decision) return false;
if (status && (d.status || 'available') !== status) return false;
if (minScore !== null && Number(d.score || 0) < minScore) return false;
if (onlyDictionary) {
const kw = Array.isArray(d.keywords) ? d.keywords.map(x => String(x).toLowerCase()) : [];
const isDict = kw.includes('brand') && kw.includes('short') && kw.includes('dictionary');
if (!isDict) return false;
}
return true;
});
const dir = sortDir === 'asc' ? 1 : -1;
arr.sort((a,b)=>{
let av = a?.[sortBy];
let bv = b?.[sortBy];
if (sortBy === 'llm_score') {
av = av === null || av === undefined ? -1 : Number(av);
bv = bv === null || bv === undefined ? -1 : Number(bv);
} else if (sortBy === 'score') {
av = Number(av || 0);
bv = Number(bv || 0);
} else if (sortBy === 'scanned_at') {
av = av ? Date.parse(av) : 0;
bv = bv ? Date.parse(bv) : 0;
} else {
av = (av || '').toString().toLowerCase();
bv = (bv || '').toString().toLowerCase();
}
if (av < bv) return -1 * dir;
if (av > bv) return 1 * dir;
return 0;
});
visibleDomains = arr;
}
function renderRows(){
applyFiltersAndSort();
const rows = document.getElementById('rows');
rows.innerHTML='';
for(const d of visibleDomains.slice(0,300)){
const tr = document.createElement('tr');
const checkUrl = domainCheckUrl(d.domain);
const offers = topOffersByTld(d.tld);
const offersHtml = offers.length
? offers.map(o => {
const link = registrarDomainUrl(o.registrar, d.domain);
const hasPrice = o.register !== null && o.renew !== null;
if (hasPrice) {
return `<div><a href='${link}' target='_blank'>${o.registrar}</a>: reg ${money(o.register)} / odn ${money(o.renew)} <span class='muted'>(2l: ${money(o.twoYear)})</span></div>`;
}
return `<div><a href='${link}' target='_blank'>${o.registrar}</a>: <span class='muted'>cena live niedostępna — kliknij, aby sprawdzić dla ${d.domain}</span></div>`;
}).join('')
: `<span class='muted'>Brak danych live dla .${d.tld}</span>`;
const llm = (d.llm_score!==null && d.llm_score!==undefined) ? `${Number(d.llm_score).toFixed(1)} ${d.decision ? `(${d.decision})` : ''}` : '—';
tr.innerHTML = `<td><a href='${checkUrl}' target='_blank'><b>${d.domain}</b></a> ${d.is_new ? `<span class='chip'>NEW</span>` : ''}</td>
<td>.${d.tld}</td>
<td>${d.score}</td>
<td>${llm}</td>
<td>${d.status || 'available'}</td>
<td class='small muted'>${d.run_id || '—'}</td>
<td class='small muted'>${d.scanned_at || '—'}</td>
<td>${offersHtml}</td>`;
rows.appendChild(tr);
}
const meta = document.getElementById('meta');
if(meta){
meta.textContent = `${metaBase} | po filtrach: ${visibleDomains.length}`;
}
}
async function refreshAll(){
await loadDomains();
await loadLivePricesForDomains(domains);
renderRows();
}
let qTimer;
let autoTimer;
function setupAutoRefresh(){
if(autoTimer) clearInterval(autoTimer);
const enabled = document.getElementById('autoRefresh').checked;
if(enabled){
autoTimer = setInterval(async()=>{
await loadDomains();
await loadLivePricesForDomains(domains);
renderRows();
}, 60000);
}
}
function setTab(mode){
viewMode = mode;
const currentBtn = document.getElementById('tabCurrent');
const allBtn = document.getElementById('tabAll');
currentBtn.classList.toggle('active', mode === 'current');
allBtn.classList.toggle('active', mode === 'all');
document.getElementById('onlyNew').disabled = (mode === 'all');
refreshAll();
}
document.getElementById('tabCurrent').addEventListener('click', ()=>setTab('current'));
document.getElementById('tabAll').addEventListener('click', ()=>setTab('all'));
document.getElementById('reload').addEventListener('click', refreshAll);
document.getElementById('q').addEventListener('input', async()=>{
clearTimeout(qTimer);
qTimer = setTimeout(async()=>{ await loadDomains(); await loadLivePricesForDomains(domains); renderRows(); }, 250);
});
document.getElementById('tld').addEventListener('change', async()=>{await loadDomains(); await loadLivePricesForDomains(domains); renderRows();});
document.getElementById('onlyNew').addEventListener('change', async()=>{await loadDomains(); await loadLivePricesForDomains(domains); renderRows();});
document.getElementById('decisionFilter').addEventListener('change', renderRows);
document.getElementById('statusFilter').addEventListener('change', renderRows);
document.getElementById('minScore').addEventListener('input', renderRows);
document.getElementById('onlyDictionary').addEventListener('change', renderRows);
document.getElementById('sortBy').addEventListener('change', renderRows);
document.getElementById('sortDir').addEventListener('change', renderRows);
document.querySelectorAll('th.sortable').forEach(th=>{
th.addEventListener('click', ()=>{
const key = th.dataset.sort;
const sortByEl = document.getElementById('sortBy');
const sortDirEl = document.getElementById('sortDir');
if(sortByEl.value === key){
sortDirEl.value = sortDirEl.value === 'asc' ? 'desc' : 'asc';
} else {
sortByEl.value = key;
sortDirEl.value = (key === 'domain' || key === 'tld' || key === 'status') ? 'asc' : 'desc';
}
renderRows();
});
});
document.getElementById('autoRefresh').addEventListener('change', setupAutoRefresh);
refreshAll();
setupAutoRefresh();
</script>
</body>
</html>