345 lines
14 KiB
HTML
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)} zł`; }
|
|
|
|
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>
|