commit 71d7bb8bf2be28b160323fe7cee7f0fd4b66a0ea Author: Adrian Miesikowski Date: Tue Feb 17 22:48:53 2026 +0100 feat: initial domain hunter panel (dynamic DB-backed) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ae82fa --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +data/domainhunter.db +data/latest-domains.json +__pycache__/ +*.pyc diff --git a/README.md b/README.md new file mode 100644 index 0000000..6e7d47e --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# Domain Hunter Panel + +Dynamiczny panel do monitoringu wolnych domen i porównania kosztów rejestracji/odnowień. + +## Co robi +- Pobiera najnowsze wyniki skanu domen z Mongo (`aiagent.domain_scans`) +- Trzyma dane panelu w SQLite (`data/domainhunter.db`) +- Udostępnia API (`api.php`) dla frontendu +- Pozwala edytować ceny rejestratorów i zapisuje je po stronie serwera +- Wspiera autopobieranie cen (heurystyki) + +## Pliki +- `index.html` — frontend panelu +- `api.php` — backend API +- `init_db.py` — inicjalizacja SQLite +- `refresh_domain_data.py` — sync Mongo -> SQLite +- `update_registrar_prices.py` — auto-update cen +- `refresh_and_publish.sh` — pełny refresh + publikacja + +## Szybki start +```bash +python3 init_db.py +python3 update_registrar_prices.py || true +python3 refresh_domain_data.py +``` diff --git a/api.php b/api.php new file mode 100644 index 0000000..fd79212 --- /dev/null +++ b/api.php @@ -0,0 +1,93 @@ + false, 'error' => 'DB not found']); + exit; +} + +try { + $db = new SQLite3($dbPath, SQLITE3_OPEN_READWRITE); +} catch (Exception $e) { + http_response_code(500); + echo json_encode(['ok' => false, 'error' => 'DB open failed']); + exit; +} + +$action = $_GET['action'] ?? 'domains'; + +if ($action === 'domains') { + $q = trim($_GET['q'] ?? ''); + $tld = trim($_GET['tld'] ?? ''); + + $runId = $db->querySingle("SELECT value FROM metadata WHERE key='latest_run_id'"); + $scannedAt = $db->querySingle("SELECT value FROM metadata WHERE key='latest_scanned_at'"); + + $sql = "SELECT domain, tld, score, status, keywords_json FROM domains WHERE run_id = :run"; + if ($q !== '') $sql .= " AND domain LIKE :q"; + if ($tld !== '') $sql .= " AND tld = :tld"; + $sql .= " ORDER BY score DESC, domain ASC LIMIT 500"; + + $st = $db->prepare($sql); + $st->bindValue(':run', $runId, SQLITE3_TEXT); + if ($q !== '') $st->bindValue(':q', '%' . $q . '%', SQLITE3_TEXT); + if ($tld !== '') $st->bindValue(':tld', $tld, SQLITE3_TEXT); + + $res = $st->execute(); + $rows = []; + while ($r = $res->fetchArray(SQLITE3_ASSOC)) { + $r['keywords'] = json_decode($r['keywords_json'] ?: '[]', true) ?: []; + unset($r['keywords_json']); + $rows[] = $r; + } + + echo json_encode([ + 'ok' => true, + 'meta' => [ + 'runId' => $runId, + 'scannedAt' => $scannedAt, + 'count' => count($rows) + ], + 'domains' => $rows, + ], JSON_UNESCAPED_UNICODE); + exit; +} + +if ($action === 'prices') { + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $raw = json_decode(file_get_contents('php://input'), true) ?: []; + $registrar = trim($raw['registrar'] ?? ''); + $tld = trim($raw['tld'] ?? ''); + $registerPrice = $raw['register_price'] ?? null; + $renewPrice = $raw['renew_price'] ?? null; + + if ($registrar === '' || $tld === '') { + http_response_code(400); + echo json_encode(['ok' => false, 'error' => 'registrar/tld required']); + exit; + } + + $st = $db->prepare("UPDATE registrar_prices SET register_price=:rp, renew_price=:rr, updated_at=datetime('now') WHERE registrar=:r AND tld=:t"); + if ($registerPrice === null || $registerPrice === '') $st->bindValue(':rp', null, SQLITE3_NULL); + else $st->bindValue(':rp', floatval($registerPrice), SQLITE3_FLOAT); + if ($renewPrice === null || $renewPrice === '') $st->bindValue(':rr', null, SQLITE3_NULL); + else $st->bindValue(':rr', floatval($renewPrice), SQLITE3_FLOAT); + $st->bindValue(':r', $registrar, SQLITE3_TEXT); + $st->bindValue(':t', $tld, SQLITE3_TEXT); + $st->execute(); + + echo json_encode(['ok' => true]); + exit; + } + + $res = $db->query("SELECT registrar, url, tld, register_price, renew_price, updated_at FROM registrar_prices ORDER BY registrar, tld"); + $items = []; + while ($r = $res->fetchArray(SQLITE3_ASSOC)) $items[] = $r; + echo json_encode(['ok' => true, 'items' => $items], JSON_UNESCAPED_UNICODE); + exit; +} + +http_response_code(400); +echo json_encode(['ok' => false, 'error' => 'unknown action']); diff --git a/data/registrars.json b/data/registrars.json new file mode 100644 index 0000000..068cfd6 --- /dev/null +++ b/data/registrars.json @@ -0,0 +1,338 @@ +{ + "note": "Ceny orientacyjne + auto-pobieranie (heurystyka z publicznych cenników). Zawsze weryfikuj przed zakupem.", + "updatedAt": "2026-02-17", + "registrars": [ + { + "name": "OVH", + "url": "https://www.ovhcloud.com/pl/domains/", + "pricing": { + "pl": { + "register": 20.69, + "renew": 20.69 + }, + "com": { + "register": 34.08, + "renew": 18.27 + }, + "ai": { + "register": 306.69, + "renew": 306.69 + } + }, + "autoPricing": { + "pl": { + "url": "https://www.ovhcloud.com/pl/domains/tld/pl/" + }, + "com": { + "url": "https://www.ovhcloud.com/pl/domains/tld/com/" + }, + "ai": { + "url": "https://www.ovhcloud.com/pl/domains/tld/ai/" + } + }, + "autoMeta": { + "pl": { + "status": "ok", + "checkedAt": "2026-02-17T21:40:45.836487Z", + "url": "https://www.ovhcloud.com/pl/domains/tld/pl/", + "registerFound": 20.69, + "renewFound": 20.69, + "registerContext": "okumentacja roadmap & changelog sprawdź, czy usługa jest dostępna w twojej lokalizacji pełna gama usług fibre połącz kilka ścieżek dostępu z otb dokumentacja roadmap & changelog domena .pl domena wszystkie rozsze", + "renewContext": "y usługa jest dostępna w twojej lokalizacji pełna gama usług fibre połącz kilka ścieżek dostępu z otb dokumentacja roadmap & changelog domena .pl domena wszystkie rozszerzenia domen domena .pl zarezerwuj twoją domenę" + }, + "com": { + "status": "ok", + "checkedAt": "2026-02-17T21:40:45.836487Z", + "url": "https://www.ovhcloud.com/pl/domains/tld/com/", + "registerFound": 34.08, + "renewFound": 18.27, + "registerContext": "ata odnowienie na 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 lata czas rejestracji natychmiastowy liczba znaków 2 do 63 znaki obsługa idn (znaki międzynarodowe) tak dnssec obsługiwane tak cena (1. rok) 34,08 pln netto 41,92 pln brutt", + "renewContext": "gelog sprawdź, czy usługa jest dostępna w twojej lokalizacji pełna gama usług fibre połącz kilka ścieżek dostępu z otb dokumentacja roadmap & changelog domena .com domena wszystkie rozszerzenia domen domena .com zare" + }, + "ai": { + "status": "ok", + "checkedAt": "2026-02-17T21:40:45.836487Z", + "url": "https://www.ovhcloud.com/pl/domains/tld/ai/", + "registerFound": 306.69, + "renewFound": 306.69, + "registerContext": "p; changelog sprawdź, czy usługa jest dostępna w twojej lokalizacji pełna gama usług fibre połącz kilka ścieżek dostępu z otb dokumentacja roadmap & changelog domena .ai domena wszystkie rozszerzenia domen domena .ai", + "renewContext": "i zarejestruj domenę .ai domena .ai jest powiązana z tematyką sztucznej inteligencji. jest to również oficjalne rozszerzenie wyspy anguilla. może zostać zarejestrowane przez dowolnego użytkownika na minimalny okres wynos" + } + } + }, + { + "name": "nazwa.pl", + "url": "https://www.nazwa.pl/domeny/", + "pricing": { + "pl": { + "register": null, + "renew": null + }, + "com": { + "register": null, + "renew": null + }, + "ai": { + "register": null, + "renew": null + } + }, + "autoPricing": { + "pl": { + "url": "https://www.nazwa.pl/domeny/rejestracja-domeny-pl/" + }, + "com": { + "url": "https://www.nazwa.pl/domeny/rejestracja-domeny-com/" + }, + "ai": { + "url": "https://www.nazwa.pl/domeny/rejestracja-domeny-ai/" + } + }, + "autoMeta": { + "pl": { + "status": "error", + "error": "HTTP Error 404: Not Found", + "checkedAt": "2026-02-17T21:40:45.836487Z", + "url": "https://www.nazwa.pl/domeny/rejestracja-domeny-pl/" + }, + "com": { + "status": "error", + "error": "HTTP Error 404: Not Found", + "checkedAt": "2026-02-17T21:40:45.836487Z", + "url": "https://www.nazwa.pl/domeny/rejestracja-domeny-com/" + }, + "ai": { + "status": "error", + "error": "HTTP Error 404: Not Found", + "checkedAt": "2026-02-17T21:40:45.836487Z", + "url": "https://www.nazwa.pl/domeny/rejestracja-domeny-ai/" + } + } + }, + { + "name": "home.pl", + "url": "https://home.pl/domeny/", + "pricing": { + "pl": { + "register": null, + "renew": null + }, + "com": { + "register": null, + "renew": null + }, + "ai": { + "register": null, + "renew": null + } + }, + "autoPricing": { + "pl": { + "url": "https://home.pl/domeny/.pl/" + }, + "com": { + "url": "https://home.pl/domeny/.com/" + }, + "ai": { + "url": "https://home.pl/domeny/.ai/" + } + }, + "autoMeta": { + "pl": { + "status": "error", + "error": "The read operation timed out", + "checkedAt": "2026-02-17T21:40:45.836487Z", + "url": "https://home.pl/domeny/.pl/" + }, + "com": { + "status": "error", + "error": "HTTP Error 404: Not Found", + "checkedAt": "2026-02-17T21:40:45.836487Z", + "url": "https://home.pl/domeny/.com/" + }, + "ai": { + "status": "error", + "error": "HTTP Error 404: Not Found", + "checkedAt": "2026-02-17T21:40:45.836487Z", + "url": "https://home.pl/domeny/.ai/" + } + } + }, + { + "name": "cyber_Folks", + "url": "https://cyberfolks.pl/domeny/", + "pricing": { + "pl": { + "register": 0.0, + "renew": 9.9 + }, + "com": { + "register": 9.9, + "renew": 9.9 + }, + "ai": { + "register": 9.9, + "renew": 9.9 + } + }, + "autoPricing": { + "pl": { + "url": "https://cyberfolks.pl/domeny/" + }, + "com": { + "url": "https://cyberfolks.pl/domeny/" + }, + "ai": { + "url": "https://cyberfolks.pl/domeny/" + } + }, + "autoMeta": { + "pl": { + "status": "ok", + "checkedAt": "2026-02-17T21:40:45.836487Z", + "url": "https://cyberfolks.pl/domeny/", + "registerFound": 0.0, + "renewFound": 9.9, + "registerContext": "ocztę i pomoże w seo! audyt wcag pomoc pomoc pomoc cyber_folks pomoc _stores pomoc _now zaloguj się pomoc pomoc cyber_folks pomoc _stores pomoc _now uruchom chat wróć zaloguj się panel klienta panel admin poczta panel _s", + "renewContext": "owadzić do niezamierzonego kierowania ruchu do konkurencji. 03 tanie domeny czy drogie domeny? mówiąc o koszcie utrzymania domen trzeba brać pod uwagę cenę rejestracji oraz cenę odnowienia domeny w kolejnych latach. w na" + }, + "com": { + "status": "ok", + "checkedAt": "2026-02-17T21:40:45.836487Z", + "url": "https://cyberfolks.pl/domeny/", + "registerFound": 9.9, + "renewFound": 9.9, + "registerContext": "ji. 03 tanie domeny czy drogie domeny? mówiąc o koszcie utrzymania domen trzeba brać pod uwagę cenę rejestracji oraz cenę odnowienia domeny w kolejnych latach. w naszej ofercie domenę .pl i inne domeny nask możesz zareje", + "renewContext": "y drogie domeny? mówiąc o koszcie utrzymania domen trzeba brać pod uwagę cenę rejestracji oraz cenę odnowienia domeny w kolejnych latach. w naszej ofercie domenę .pl i inne domeny nask możesz zarejestrować za 9.90 zł net" + }, + "ai": { + "status": "ok", + "checkedAt": "2026-02-17T21:40:45.836487Z", + "url": "https://cyberfolks.pl/domeny/", + "registerFound": 9.9, + "renewFound": 9.9, + "registerContext": "ji. 03 tanie domeny czy drogie domeny? mówiąc o koszcie utrzymania domen trzeba brać pod uwagę cenę rejestracji oraz cenę odnowienia domeny w kolejnych latach. w naszej ofercie domenę .pl i inne domeny nask możesz zareje", + "renewContext": "y drogie domeny? mówiąc o koszcie utrzymania domen trzeba brać pod uwagę cenę rejestracji oraz cenę odnowienia domeny w kolejnych latach. w naszej ofercie domenę .pl i inne domeny nask możesz zarejestrować za 9.90 zł net" + } + } + }, + { + "name": "Aftermarket", + "url": "https://aftermarket.pl/", + "pricing": { + "pl": { + "register": null, + "renew": null + }, + "com": { + "register": null, + "renew": null + }, + "ai": { + "register": null, + "renew": null + } + }, + "autoPricing": { + "pl": { + "url": "https://aftermarket.pl/domeny/" + }, + "com": { + "url": "https://aftermarket.pl/domeny/" + }, + "ai": { + "url": "https://aftermarket.pl/domeny/" + } + }, + "autoMeta": { + "pl": { + "status": "ok", + "checkedAt": "2026-02-17T21:40:45.836487Z", + "url": "https://aftermarket.pl/domeny/", + "registerFound": null, + "renewFound": null, + "registerContext": null, + "renewContext": null + }, + "com": { + "status": "ok", + "checkedAt": "2026-02-17T21:40:45.836487Z", + "url": "https://aftermarket.pl/domeny/", + "registerFound": null, + "renewFound": null, + "registerContext": null, + "renewContext": null + }, + "ai": { + "status": "ok", + "checkedAt": "2026-02-17T21:40:45.836487Z", + "url": "https://aftermarket.pl/domeny/", + "registerFound": null, + "renewFound": null, + "registerContext": null, + "renewContext": null + } + } + }, + { + "name": "Porkbun", + "url": "https://porkbun.com/", + "pricing": { + "pl": { + "register": 26.26, + "renew": 26.26 + }, + "com": { + "register": null, + "renew": null + }, + "ai": { + "register": null, + "renew": null + } + }, + "autoPricing": { + "pl": { + "url": "https://porkbun.com/tld/pl" + }, + "com": { + "url": "https://porkbun.com/tld/com" + }, + "ai": { + "url": "https://porkbun.com/tld/ai" + } + }, + "autoMeta": { + "pl": { + "status": "ok", + "checkedAt": "2026-02-17T21:40:45.836487Z", + "url": "https://porkbun.com/tld/pl", + "registerFound": 26.26, + "renewFound": 26.26, + "registerContext": "st deals page. a b c d e f g h i j k l m n o p q r s t u v w x x y z - idn extension registration renewal transfer a back to top .abogado $ 26.26 $ 26.26 $ 26.26 .ac $46.65 1st yr sale! $ 26.06 $ 46.65 $ 46.65 .ac.nz $ 1", + "renewContext": "test deals page. a b c d e f g h i j k l m n o p q r s t u v w x x y z - idn extension registration renewal transfer a back to top .abogado $ 26.26 $ 26.26 $ 26.26 .ac $46.65 1st yr sale! $ 26.06 $ 46.65 $ 46.65 .ac.nz $" + }, + "com": { + "status": "ok", + "checkedAt": "2026-02-17T21:40:45.836487Z", + "url": "https://porkbun.com/tld/com", + "registerFound": null, + "renewFound": null, + "registerContext": null, + "renewContext": null + }, + "ai": { + "status": "ok", + "checkedAt": "2026-02-17T21:40:45.836487Z", + "url": "https://porkbun.com/tld/ai", + "registerFound": null, + "renewFound": null, + "registerContext": null, + "renewContext": null + } + } + } + ], + "autoLastRunAt": "2026-02-17T21:40:45.836487Z", + "autoUpdatedFields": 14 +} \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..c193026 --- /dev/null +++ b/index.html @@ -0,0 +1,204 @@ + + + + + + Domain Hunter Panel + + + +
+

Domain Hunter Panel

+

Dynamicznie z DB: domeny z Mongo + ceny rejestratorów (zapisywane na serwerze).

+ +
+
+

Filtr domen

+
+ + + +
+

+
+ +
+

Cennik rejestratorów

+

Edycja zapisuje się bezpośrednio na serwerze (SQLite), bez localStorage.

+
+
+
+ +
+

TOP domen + najlepszy koszt

+
+ + + +
DomenaScoreNajtaniej kupiszNajtaniej odnowisz2 lata
+
+
+
+ + + + diff --git a/init_db.py b/init_db.py new file mode 100644 index 0000000..5aeedbc --- /dev/null +++ b/init_db.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +import sqlite3 +from pathlib import Path + +DB = Path('/home/szmyt/.openclaw/workspace/domain-panel/data/domainhunter.db') +DB.parent.mkdir(parents=True, exist_ok=True) + +con = sqlite3.connect(DB) +cur = con.cursor() +cur.executescript(''' +CREATE TABLE IF NOT EXISTS metadata ( + key TEXT PRIMARY KEY, + value TEXT +); + +CREATE TABLE IF NOT EXISTS domains ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + run_id TEXT, + scanned_at TEXT, + domain TEXT, + tld TEXT, + score INTEGER, + status TEXT, + keywords_json TEXT +); +CREATE INDEX IF NOT EXISTS idx_domains_run ON domains(run_id); +CREATE INDEX IF NOT EXISTS idx_domains_domain ON domains(domain); + +CREATE TABLE IF NOT EXISTS registrar_prices ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + registrar TEXT, + url TEXT, + tld TEXT, + register_price REAL, + renew_price REAL, + updated_at TEXT, + UNIQUE(registrar, tld) +); +''') +con.commit() +con.close() +print(f'OK init {DB}') diff --git a/refresh_and_publish.sh b/refresh_and_publish.sh new file mode 100755 index 0000000..9afc113 --- /dev/null +++ b/refresh_and_publish.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euo pipefail +python3 /home/szmyt/.openclaw/workspace/domain-panel/init_db.py +python3 /home/szmyt/.openclaw/workspace/domain-panel/update_registrar_prices.py || true +python3 /home/szmyt/.openclaw/workspace/domain-panel/refresh_domain_data.py +sudo rsync -a --delete /home/szmyt/.openclaw/workspace/domain-panel/ /var/www/domain-panel/ +sudo chown -R www-data:www-data /var/www/domain-panel +sudo find /var/www/domain-panel -type d -exec chmod 755 {} + +sudo find /var/www/domain-panel -type f -exec chmod 644 {} + +echo "Published domain panel to /var/www/domain-panel" diff --git a/refresh_domain_data.py b/refresh_domain_data.py new file mode 100755 index 0000000..3d7dbac --- /dev/null +++ b/refresh_domain_data.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +import json +import sqlite3 +import subprocess +from pathlib import Path + +ENV_PATH = Path('/home/szmyt/docker/databases/.env') +REG_PATH = Path('/home/szmyt/.openclaw/workspace/domain-panel/data/registrars.json') +DB_PATH = Path('/home/szmyt/.openclaw/workspace/domain-panel/data/domainhunter.db') + + +def read_env(path: Path): + data = {} + if not path.exists(): + return data + for line in path.read_text(encoding='utf-8').splitlines(): + line = line.strip() + if not line or line.startswith('#') or '=' not in line: + continue + k, v = line.split('=', 1) + data[k.strip()] = v.strip() + return data + + +def fetch_latest_from_mongo(user, pwd): + js = r''' +const dbx = db.getSiblingDB("aiagent"); +const first = dbx.domain_scans.find().sort({scannedAt:-1}).limit(1).toArray(); +if (!first.length) { + print(JSON.stringify({runId:null,scannedAt:null,totalAvailable:0,domains:[]})); +} else { + const runId = first[0].runId; + const scannedAt = first[0].scannedAt; + const docs = dbx.domain_scans.find({runId: runId, available: true}).sort({score:-1}).limit(500).toArray().map(d => ({ + domain: d.domain, + tld: d.tld, + score: d.score, + status: d.status, + keywords: d.keywords || [] + })); + print(JSON.stringify({runId, scannedAt, totalAvailable: docs.length, domains: docs})); +} +''' + cmd = [ + 'docker', 'exec', 'mongo', 'mongosh', '--quiet', + '--username', user, + '--password', pwd, + '--authenticationDatabase', 'admin', + '--eval', js, + ] + p = subprocess.run(cmd, capture_output=True, text=True, timeout=60) + if p.returncode != 0: + raise RuntimeError((p.stderr or p.stdout or 'mongosh failed').strip()) + out = p.stdout.strip().splitlines()[-1].strip() + return json.loads(out) + + +def upsert_metadata(cur, key, value): + cur.execute('INSERT INTO metadata(key,value) VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value', (key, value)) + + +def main(): + env = read_env(ENV_PATH) + user = env.get('MONGO_INITDB_ROOT_USERNAME', 'root') + pwd = env.get('MONGO_INITDB_ROOT_PASSWORD', '') + + latest = fetch_latest_from_mongo(user, pwd) + + DB_PATH.parent.mkdir(parents=True, exist_ok=True) + con = sqlite3.connect(DB_PATH) + cur = con.cursor() + + # ensure schema exists + cur.executescript(''' + CREATE TABLE IF NOT EXISTS metadata (key TEXT PRIMARY KEY, value TEXT); + CREATE TABLE IF NOT EXISTS domains ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + run_id TEXT, + scanned_at TEXT, + domain TEXT, + tld TEXT, + score INTEGER, + status TEXT, + keywords_json TEXT + ); + CREATE INDEX IF NOT EXISTS idx_domains_run ON domains(run_id); + CREATE INDEX IF NOT EXISTS idx_domains_domain ON domains(domain); + CREATE TABLE IF NOT EXISTS registrar_prices ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + registrar TEXT, + url TEXT, + tld TEXT, + register_price REAL, + renew_price REAL, + updated_at TEXT, + UNIQUE(registrar, tld) + ); + ''') + + run_id = latest.get('runId') + scanned_at = latest.get('scannedAt') + + if run_id: + cur.execute('DELETE FROM domains WHERE run_id = ?', (run_id,)) + for d in latest.get('domains', []): + cur.execute('''INSERT INTO domains(run_id, scanned_at, domain, tld, score, status, keywords_json) + VALUES(?,?,?,?,?,?,?)''', ( + run_id, + scanned_at, + d.get('domain'), + d.get('tld'), + int(d.get('score') or 0), + d.get('status'), + json.dumps(d.get('keywords') or [], ensure_ascii=False), + )) + + upsert_metadata(cur, 'latest_run_id', run_id or '') + upsert_metadata(cur, 'latest_scanned_at', scanned_at or '') + upsert_metadata(cur, 'latest_total_available', str(latest.get('totalAvailable', 0))) + + if REG_PATH.exists(): + reg = json.loads(REG_PATH.read_text(encoding='utf-8')) + for r in reg.get('registrars', []): + name = r.get('name') + url = r.get('url') + pricing = r.get('pricing', {}) + for tld in ['pl', 'com', 'ai']: + p = pricing.get(tld, {}) + cur.execute('''INSERT INTO registrar_prices(registrar,url,tld,register_price,renew_price,updated_at) + VALUES(?,?,?,?,?,?) + ON CONFLICT(registrar,tld) DO UPDATE SET + url=excluded.url, + register_price=excluded.register_price, + renew_price=excluded.renew_price, + updated_at=excluded.updated_at''', ( + name, url, tld, p.get('register'), p.get('renew'), reg.get('autoLastRunAt') or reg.get('updatedAt') or '' + )) + + con.commit() + con.close() + print(f"OK: db refreshed {DB_PATH} | available={latest.get('totalAvailable',0)}") + + +if __name__ == '__main__': + main() diff --git a/update_registrar_prices.py b/update_registrar_prices.py new file mode 100755 index 0000000..84c3e59 --- /dev/null +++ b/update_registrar_prices.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +import json +import re +import urllib.request +from datetime import datetime, timezone +from pathlib import Path + +REG_PATH = Path('/home/szmyt/.openclaw/workspace/domain-panel/data/registrars.json') +TIMEOUT = 8 + +MONEY_RE = re.compile(r'(\d{1,4}(?:[\.,]\d{1,2})?)\s*(zł|pln|eur|€|usd|\$)', re.IGNORECASE) + +REGISTER_HINTS = ["rejestr", "rejestracja", "new", "first year", "1 rok", "1 rok"] +RENEW_HINTS = ["odnow", "renew", "renewal", "kolejny", "next year", "2 rok"] + + +def fetch_text(url: str) -> str: + req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0 DomainHunterBot/1.0"}) + with urllib.request.urlopen(req, timeout=TIMEOUT) as resp: + raw = resp.read() + text = raw.decode('utf-8', errors='ignore') + text = re.sub(r'', ' ', text, flags=re.IGNORECASE) + text = re.sub(r'', ' ', text, flags=re.IGNORECASE) + text = re.sub(r'<[^>]+>', ' ', text) + text = re.sub(r'\s+', ' ', text) + return text + + +def to_float(num: str): + try: + return float(num.replace(',', '.')) + except Exception: + return None + + +def find_price(text: str, tld: str, kind: str): + hints = REGISTER_HINTS if kind == 'register' else RENEW_HINTS + tld_token = f'.{tld.lower()}' + + # 1) szukaj okna z tld + hint + for m in re.finditer(re.escape(tld_token), text, flags=re.IGNORECASE): + start = max(0, m.start() - 240) + end = min(len(text), m.end() + 240) + chunk = text[start:end].lower() + if any(h in chunk for h in hints): + m2 = MONEY_RE.search(chunk) + if m2: + val = to_float(m2.group(1)) + if val is not None: + return val, chunk[:220] + + # 2) fallback: hint + cena + for h in hints: + for mh in re.finditer(re.escape(h), text, flags=re.IGNORECASE): + start = max(0, mh.start() - 100) + end = min(len(text), mh.end() + 200) + chunk = text[start:end].lower() + m2 = MONEY_RE.search(chunk) + if m2: + val = to_float(m2.group(1)) + if val is not None: + return val, chunk[:220] + + return None, None + + +def main(): + data = json.loads(REG_PATH.read_text(encoding='utf-8')) + now = datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z') + + updated_count = 0 + + for reg in data.get('registrars', []): + auto = reg.get('autoPricing', {}) + for tld in ['pl', 'com', 'ai']: + rule = auto.get(tld) + if not rule or not rule.get('url'): + continue + url = rule['url'] + try: + text = fetch_text(url) + except Exception as e: + reg.setdefault('autoMeta', {})[tld] = { + 'status': 'error', + 'error': str(e), + 'checkedAt': now, + 'url': url, + } + continue + + pricing = reg.setdefault('pricing', {}).setdefault(tld, {'register': None, 'renew': None}) + reg_val, reg_ctx = find_price(text, tld, 'register') + ren_val, ren_ctx = find_price(text, tld, 'renew') + + if reg_val is not None: + pricing['register'] = reg_val + updated_count += 1 + if ren_val is not None: + pricing['renew'] = ren_val + updated_count += 1 + + reg.setdefault('autoMeta', {})[tld] = { + 'status': 'ok', + 'checkedAt': now, + 'url': url, + 'registerFound': reg_val, + 'renewFound': ren_val, + 'registerContext': reg_ctx, + 'renewContext': ren_ctx, + } + + data['updatedAt'] = now[:10] + data['autoLastRunAt'] = now + data['autoUpdatedFields'] = updated_count + REG_PATH.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding='utf-8') + print(f'OK: updated fields={updated_count}') + + +if __name__ == '__main__': + main()