feat: live registrar pricing endpoint and frontend integration
Some checks are pending
CI / lint (push) Waiting to run
Deploy / deploy (push) Waiting to run

This commit is contained in:
Adrian Miesikowski 2026-02-18 21:08:22 +01:00
parent 1049ccf5a7
commit 16b49ad833
2 changed files with 189 additions and 24 deletions

148
api.php
View File

@ -18,6 +18,71 @@ try {
$action = $_GET['action'] ?? 'domains'; $action = $_GET['action'] ?? 'domains';
function dh_strip_html_text(string $html): string {
$html = preg_replace('/<script\b[^>]*>[\s\S]*?<\/script>/i', ' ', $html);
$html = preg_replace('/<style\b[^>]*>[\s\S]*?<\/style>/i', ' ', $html);
$text = strip_tags($html ?? '');
$text = preg_replace('/\s+/u', ' ', $text ?? '');
return trim($text ?? '');
}
function dh_fetch_text(string $url): ?string {
$ctx = stream_context_create([
'http' => [
'timeout' => 12,
'user_agent' => 'Mozilla/5.0 DomainHunterPanel/1.0',
'follow_location' => 1,
],
'ssl' => [
'verify_peer' => true,
'verify_peer_name' => true,
]
]);
$raw = @file_get_contents($url, false, $ctx);
if ($raw === false) return null;
return dh_strip_html_text($raw);
}
function dh_find_price(string $text, string $tld, string $kind): ?float {
$hintsRegister = ['rejestr', 'rejestracja', 'new', 'first year', '1 rok'];
$hintsRenew = ['odnow', 'renew', 'renewal', 'kolejny', 'next year', '2 rok'];
$hints = $kind === 'renew' ? $hintsRenew : $hintsRegister;
$token = '.' . strtolower($tld);
$len = mb_strlen($text, 'UTF-8');
$pos = 0;
while (($idx = mb_stripos($text, $token, $pos, 'UTF-8')) !== false) {
$start = max(0, $idx - 240);
$chunk = mb_substr($text, $start, 520, 'UTF-8');
$chunkLow = mb_strtolower($chunk, 'UTF-8');
$ok = false;
foreach ($hints as $h) {
if (mb_strpos($chunkLow, $h, 0, 'UTF-8') !== false) { $ok = true; break; }
}
if ($ok && preg_match('/(\d{1,4}(?:[\.,]\d{1,2})?)\s*(zł|pln|eur|€|usd|\$)/iu', $chunkLow, $m)) {
return floatval(str_replace(',', '.', $m[1]));
}
$pos = $idx + mb_strlen($token, 'UTF-8');
if ($pos >= $len) break;
}
foreach ($hints as $h) {
$pos = 0;
while (($idx = mb_stripos($text, $h, $pos, 'UTF-8')) !== false) {
$start = max(0, $idx - 100);
$chunk = mb_substr($text, $start, 320, 'UTF-8');
if (preg_match('/(\d{1,4}(?:[\.,]\d{1,2})?)\s*(zł|pln|eur|€|usd|\$)/iu', $chunk, $m)) {
return floatval(str_replace(',', '.', $m[1]));
}
$pos = $idx + mb_strlen($h, 'UTF-8');
if ($pos >= $len) break;
}
}
return null;
}
if ($action === 'domains') { if ($action === 'domains') {
$q = trim($_GET['q'] ?? ''); $q = trim($_GET['q'] ?? '');
$tld = trim($_GET['tld'] ?? ''); $tld = trim($_GET['tld'] ?? '');
@ -82,6 +147,89 @@ if ($action === 'domains') {
exit; exit;
} }
if ($action === 'live_prices') {
$domain = trim($_GET['domain'] ?? '');
$tld = strtolower(trim($_GET['tld'] ?? ''));
if ($domain !== '' && $tld === '' && strpos($domain, '.') !== false) {
$parts = explode('.', $domain);
$tld = strtolower(end($parts));
}
if ($tld === '') {
http_response_code(400);
echo json_encode(['ok' => false, 'error' => 'domain or tld required']);
exit;
}
$regPath = __DIR__ . '/data/registrars.json';
if (!file_exists($regPath)) {
http_response_code(500);
echo json_encode(['ok' => false, 'error' => 'registrars.json missing']);
exit;
}
$regData = json_decode(file_get_contents($regPath), true) ?: [];
$cachePath = __DIR__ . '/data/live-price-cache.json';
$cacheTtl = 1800;
$cache = [];
if (file_exists($cachePath)) {
$cache = json_decode(file_get_contents($cachePath), true) ?: [];
}
$now = time();
$items = [];
foreach (($regData['registrars'] ?? []) as $reg) {
$name = $reg['name'] ?? '';
if ($name === '') continue;
$autoUrl = $reg['autoPricing'][$tld]['url'] ?? null;
$baseUrl = $reg['url'] ?? null;
$key = strtolower($name) . '|' . $tld;
$entry = $cache[$key] ?? null;
if (!$entry || (($entry['ts'] ?? 0) + $cacheTtl < $now)) {
$register = null;
$renew = null;
$sourceUrl = $autoUrl ?: $baseUrl;
if ($sourceUrl) {
$text = dh_fetch_text($sourceUrl);
if ($text !== null) {
$register = dh_find_price($text, $tld, 'register');
$renew = dh_find_price($text, $tld, 'renew');
}
}
$entry = [
'register_price' => $register,
'renew_price' => $renew,
'source_url' => $sourceUrl,
'ts' => $now,
];
$cache[$key] = $entry;
}
$items[] = [
'registrar' => $name,
'tld' => $tld,
'register_price' => $entry['register_price'] ?? null,
'renew_price' => $entry['renew_price'] ?? null,
'source_url' => $entry['source_url'] ?? null,
'live' => true,
'checked_at' => gmdate('c', intval($entry['ts'] ?? $now)),
];
}
@file_put_contents($cachePath, json_encode($cache, JSON_UNESCAPED_UNICODE));
echo json_encode([
'ok' => true,
'domain' => $domain,
'tld' => $tld,
'cached_seconds' => $cacheTtl,
'items' => $items,
], JSON_UNESCAPED_UNICODE);
exit;
}
if ($action === 'prices') { if ($action === 'prices') {
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$raw = json_decode(file_get_contents('php://input'), true) ?: []; $raw = json_decode(file_get_contents('php://input'), true) ?: [];

View File

@ -34,7 +34,7 @@
<body> <body>
<div class="wrap"> <div class="wrap">
<h1>Domain Hunter Panel</h1> <h1>Domain Hunter Panel</h1>
<p class="muted">Dynamicznie z DB: domeny z Mongo + status nowości w kolejnych skanach.</p> <p class="muted">Domeny z Mongo + ceny live per TLD pobierane dynamicznie z rejestratorów (z krótkim cache).</p>
<div class="card"> <div class="card">
<h2>Filtr domen</h2> <h2>Filtr domen</h2>
@ -63,7 +63,7 @@
<script> <script>
let domains = []; let domains = [];
let priceItems = []; let livePriceByTld = {};
function money(v){ return (v===null || v===undefined || Number.isNaN(Number(v))) ? '—' : `${Number(v).toFixed(2)} zł`; } function money(v){ return (v===null || v===undefined || Number.isNaN(Number(v))) ? '—' : `${Number(v).toFixed(2)} zł`; }
@ -84,24 +84,37 @@ function registrarDomainUrl(name, domain){
return domainCheckUrl(domain); return domainCheckUrl(domain);
} }
async function loadPrices(){ async function loadLivePricesForDomains(domainsList){
const u = new URL('./api.php', location.href); const tlds = [...new Set((domainsList||[]).map(x => x.tld).filter(Boolean))];
u.searchParams.set('action','prices'); const next = {};
const d = await (await fetch(u)).json(); for (const tld of tlds){
priceItems = d.items || []; 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){ function topOffersByTld(tld){
const offers = priceItems const offers = (livePriceByTld[tld] || []).slice();
.filter(x => x.tld === tld && x.register_price !== null && x.renew_price !== null) offers.sort((a,b)=>{
.map(x => ({ const av = a.twoYear === null ? Number.POSITIVE_INFINITY : a.twoYear;
registrar: x.registrar, const bv = b.twoYear === null ? Number.POSITIVE_INFINITY : b.twoYear;
register: Number(x.register_price), return av - bv;
renew: Number(x.renew_price), });
twoYear: Number(x.register_price) + Number(x.renew_price)
}))
.filter(x => !Number.isNaN(x.register) && !Number.isNaN(x.renew))
.sort((a,b) => a.twoYear - b.twoYear);
return offers.slice(0,3); return offers.slice(0,3);
} }
@ -129,9 +142,13 @@ function renderRows(){
const offersHtml = offers.length const offersHtml = offers.length
? offers.map(o => { ? offers.map(o => {
const link = registrarDomainUrl(o.registrar, d.domain); const link = registrarDomainUrl(o.registrar, d.domain);
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>`; 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('') }).join('')
: `<span class='muted'>Brak danych cenowych dla .${d.tld}</span>`; : `<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})` : ''}` : '—'; 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> tr.innerHTML = `<td><a href='${checkUrl}' target='_blank'><b>${d.domain}</b></a> ${d.is_new ? `<span class='chip'>NEW</span>` : ''}</td>
@ -146,7 +163,7 @@ function renderRows(){
async function refreshAll(){ async function refreshAll(){
await loadDomains(); await loadDomains();
await loadPrices(); await loadLivePricesForDomains(domains);
renderRows(); renderRows();
} }
@ -159,7 +176,7 @@ function setupAutoRefresh(){
if(enabled){ if(enabled){
autoTimer = setInterval(async()=>{ autoTimer = setInterval(async()=>{
await loadDomains(); await loadDomains();
await loadPrices(); await loadLivePricesForDomains(domains);
renderRows(); renderRows();
}, 60000); }, 60000);
} }
@ -168,10 +185,10 @@ function setupAutoRefresh(){
document.getElementById('reload').addEventListener('click', refreshAll); document.getElementById('reload').addEventListener('click', refreshAll);
document.getElementById('q').addEventListener('input', async()=>{ document.getElementById('q').addEventListener('input', async()=>{
clearTimeout(qTimer); clearTimeout(qTimer);
qTimer = setTimeout(async()=>{ await loadDomains(); renderRows(); }, 250); qTimer = setTimeout(async()=>{ await loadDomains(); await loadLivePricesForDomains(domains); renderRows(); }, 250);
}); });
document.getElementById('tld').addEventListener('change', async()=>{await loadDomains(); renderRows();}); document.getElementById('tld').addEventListener('change', async()=>{await loadDomains(); await loadLivePricesForDomains(domains); renderRows();});
document.getElementById('onlyNew').addEventListener('change', async()=>{await loadDomains(); renderRows();}); document.getElementById('onlyNew').addEventListener('change', async()=>{await loadDomains(); await loadLivePricesForDomains(domains); renderRows();});
document.getElementById('autoRefresh').addEventListener('change', setupAutoRefresh); document.getElementById('autoRefresh').addEventListener('change', setupAutoRefresh);
refreshAll(); refreshAll();