1 line
68 KiB
JSON
1 line
68 KiB
JSON
[{"uuid": "019cc4ed-3125-71f3-91d7-6833d66e371b", "name": "AuctionDeals", "description": "\"We are building an industrial-grade Auction Sniper that uses AI-driven logic to hunt for underpriced electronics across the web. It doesn't just 'search' for items; it uses a scoring system to distinguish between actual high-value devices and cheap accessories, ensuring it only pings us for real profit opportunities.\"", "is_private": true, "is_starter_project": false, "prompt_template": "", "created_at": "2026-03-06T20:53:24.895692+00:00", "updated_at": "2026-03-06T20:53:24.895692+00:00", "creator": {"uuid": "46ff33bf-4f36-4557-bbf6-52b84de5893d", "full_name": "Abbas"}, "docs": [{"uuid": "e9c1d921-c4ed-4905-becd-9d9c4bdac437", "filename": "worker.py", "content": "\"\"\"\nGhost Node \u2014 Worker\nThree-thread architecture:\n Thread A \u2192 FastAPI dashboard (port 8000)\n Thread B \u2192 Async Playwright scraper (nuclear_engine)\n Thread C \u2192 Telegram C2 polling loop\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport json\nimport os\nimport platform\nimport random\nimport re\nimport sys\nimport threading\nimport time\nfrom contextlib import asynccontextmanager\nfrom datetime import datetime\nfrom typing import Any, Optional\n\nimport httpx\nimport uvicorn\nfrom fastapi import Depends, FastAPI, Request\nfrom fastapi.middleware.cors import CORSMiddleware\nfrom fastapi.responses import FileResponse, HTMLResponse, JSONResponse\nfrom fastapi.staticfiles import StaticFiles\nfrom playwright.async_api import async_playwright\nfrom sqlalchemy.orm import Session\n\nfrom database import SessionLocal, get_db\nfrom models import Config, Keyword, Listing, TargetSite, calculate_attribute_score, seed_database\n\n# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Bootstrap\n# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nseed_database() # idempotent \u2014 only seeds if tables are empty\n\n# Shared mutable state (thread-safe reads are fine for these primitives)\n_stats: dict[str, Any] = {\n \"total_scanned\": 0,\n \"total_alerts\": 0,\n \"last_cycle\": \"Never\",\n \"engine_status\": \"Idle\",\n \"uptime_start\": time.time(),\n}\n\n_rotating_agents: list[str] = [\n \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 \"\n \"(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36\",\n \"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0\",\n \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 \"\n \"(KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0\",\n \"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_4_1) AppleWebKit/605.1.15 \"\n \"(KHTML, like Gecko) Version/17.4.1 Safari/605.1.15\",\n \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 \"\n \"(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36\",\n]\n\n# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Telegram helpers\n# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ndef _get_config(key: str, default: str = \"\") -> str:\n db = SessionLocal()\n try:\n row = db.query(Config).filter(Config.key == key).first()\n return row.value if row and row.value else default\n finally:\n db.close()\n\n\nasync def send_telegram(message: str) -> bool:\n \"\"\"\n Pull token + chat_id fresh from the DB on every call so Settings-tab\n changes are immediately active without a restart.\n Prints the full Telegram error body so 401/404 reasons are visible.\n \"\"\"\n token = _get_config(\"telegram_token\")\n chat_id = _get_config(\"telegram_chat_id\")\n\n if not token or not chat_id:\n print(\"[Telegram] \u26a0\ufe0f No token/chat_id in DB \u2014 save Settings first.\")\n return False\n\n url = f\"https://api.telegram.org/bot{token}/sendMessage\"\n try:\n async with httpx.AsyncClient(timeout=15) as client:\n r = await client.post(\n url,\n data={\"chat_id\": chat_id, \"text\": message, \"parse_mode\": \"HTML\"},\n )\n if r.status_code == 200:\n print(f\"[Telegram] \u2705 Alert sent to chat {chat_id}\")\n return True\n else:\n # \u2500\u2500 Critical: print the full Telegram JSON error body \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n print(\n f\"[Telegram] \u274c HTTP {r.status_code} \u2014 \"\n f\"token='{token[:12]}\u2026' chat='{chat_id}'\\n\"\n f\" Telegram says: {r.text}\"\n )\n return False\n except httpx.TimeoutException:\n print(\"[Telegram] \u274c Request timed out \u2014 check network.\")\n return False\n except httpx.RequestError as exc:\n print(f\"[Telegram] \u274c Network error: {exc}\")\n return False\n except Exception as exc:\n print(f\"[Telegram] \u274c Unexpected error: {type(exc).__name__}: {exc}\")\n return False\n\n\nasync def get_telegram_updates(offset: int) -> list[dict]:\n token = _get_config(\"telegram_token\")\n if not token:\n return []\n url = f\"https://api.telegram.org/bot{token}/getUpdates\"\n try:\n async with httpx.AsyncClient(timeout=20) as client:\n r = await client.get(url, params={\"offset\": offset, \"timeout\": 10})\n if r.status_code == 200:\n return r.json().get(\"result\", [])\n else:\n print(f\"[Telegram C2] \u274c getUpdates HTTP {r.status_code}: {r.text}\")\n except Exception as exc:\n print(f\"[Telegram C2] \u274c getUpdates error: {exc}\")\n return []\n\n\n# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Thread C \u2014 Telegram C2 Polling\n# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nasync def telegram_c2_loop() -> None:\n offset = 0\n print(\"[Thread C] Telegram C2 online.\")\n while True:\n try:\n updates = await get_telegram_updates(offset)\n for upd in updates:\n offset = upd[\"update_id\"] + 1\n msg = upd.get(\"message\", {})\n text = msg.get(\"text\", \"\").strip()\n chat_id_upd = msg.get(\"chat\", {}).get(\"id\")\n if not chat_id_upd:\n continue\n\n if text == \"/status\":\n uptime_secs = int(time.time() - _stats[\"uptime_start\"])\n h, rem = divmod(uptime_secs, 3600)\n m, s = divmod(rem, 60)\n report = (\n \"\ud83d\udd75\ufe0f <b>Ghost Node \u2014 Health Report</b>\\n\"\n f\"\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\\n\"\n f\"\ud83d\udfe2 Engine: {_stats['engine_status']}\\n\"\n f\"\ud83d\udce1 Scanned: {_stats['total_scanned']} listings\\n\"\n f\"\ud83d\udea8 Alerts sent: {_stats['total_alerts']}\\n\"\n f\"\ud83d\udd04 Last cycle: {_stats['last_cycle']}\\n\"\n f\"\u23f1\ufe0f Uptime: {h:02d}h {m:02d}m {s:02d}s\\n\"\n f\"\ud83d\udda5\ufe0f Host OS: {platform.system()} {platform.release()}\"\n )\n await send_telegram(report)\n\n elif text == \"/pause\":\n _stats[\"engine_status\"] = \"Paused\"\n await send_telegram(\"\u23f8\ufe0f Engine paused.\")\n\n elif text == \"/resume\":\n _stats[\"engine_status\"] = \"Running\"\n await send_telegram(\"\u25b6\ufe0f Engine resumed.\")\n\n elif text == \"/listings\":\n db = SessionLocal()\n try:\n rows = db.query(Listing).order_by(Listing.timestamp.desc()).limit(5).all()\n if rows:\n lines = \"\\n\".join(\n f\"\u2022 {r.title[:40]} \u2014 \u00a3{r.price or '?'} (score {r.score})\"\n for r in rows\n )\n await send_telegram(f\"\ud83d\udccb <b>Last 5 Listings:</b>\\n{lines}\")\n else:\n await send_telegram(\"No listings found yet.\")\n finally:\n db.close()\n\n except Exception as exc:\n print(f\"[Thread C] Error: {exc}\")\n\n await asyncio.sleep(3)\n\n\n# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Thread B \u2014 Nuclear Scraper Engine\n# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ndef _extract_price(text: str) -> Optional[float]:\n \"\"\"Pull the first decimal number from a price string.\"\"\"\n m = re.search(r\"[\\d,]+\\.?\\d*\", text.replace(\",\", \"\"))\n if m:\n try:\n return float(m.group().replace(\",\", \"\"))\n except ValueError:\n pass\n return None\n\n\nasync def scrape_site(\n page,\n site: TargetSite,\n keyword: Keyword,\n db: Session,\n) -> int:\n \"\"\"\n Navigate to a target site, mimic human search, parse results.\n Returns the count of qualifying new listings saved.\n \"\"\"\n new_count = 0\n\n try:\n # \u2500\u2500 Navigate to homepage first (human-mimicry) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n homepage = re.sub(r\"/search.*|/sch.*\", \"\", site.url_template.split(\"{\")[0])\n if not homepage.startswith(\"http\"):\n homepage = \"https://\" + homepage\n\n await page.goto(homepage, timeout=60_000, wait_until=\"domcontentloaded\")\n\n # \u2500\u2500 Use search box if selector provided, else go direct \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n if site.search_selector and site.search_selector.strip():\n try:\n await page.wait_for_selector(site.search_selector, timeout=10_000)\n await page.click(site.search_selector)\n await page.fill(site.search_selector, \"\") # clear first\n await page.type(\n site.search_selector,\n keyword.term,\n delay=random.randint(80, 200), # human typing\n )\n await page.keyboard.press(\"Enter\")\n await page.wait_for_load_state(\"networkidle\", timeout=60_000)\n except Exception as sel_exc:\n # Fallback: build direct URL \u2014 log it so user can debug\n url = site.url_template.replace(\"{keyword}\", keyword.term.replace(\" \", \"+\"))\n print(f\"[Scraper] Selector fallback \u2192 {url}\")\n await page.goto(url, timeout=60_000, wait_until=\"networkidle\")\n else:\n # Direct URL mode \u2014 substitute {keyword} and log the final URL\n url = site.url_template.replace(\"{keyword}\", keyword.term.replace(\" \", \"+\"))\n print(f\"[Scraper] \u2192 {site.name} | '{keyword.term}' \u2192 {url}\")\n await page.goto(url, timeout=60_000, wait_until=\"networkidle\")\n\n # \u2500\u2500 Collect listing elements \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n # Generic selectors that work across most auction/listing sites\n listing_selectors = [\n \"li.s-item\", # eBay\n \".item-cell\",\n \"article.product-pod\",\n \"div.listing-item\",\n \"[data-listing-id]\",\n \"div[class*='result']\",\n \"li[class*='product']\",\n ]\n\n items = []\n for sel in listing_selectors:\n items = await page.query_selector_all(sel)\n if items:\n break\n\n if not items:\n # Last-resort: grab all anchor text blocks\n items = await page.query_selector_all(\"a[href*='itm'], a[href*='listing']\")\n\n for item in items[:30]: # cap at 30 per page\n try:\n title_el = await item.query_selector(\n \".s-item__title, h2, h3, .title, [class*='title']\"\n )\n price_el = await item.query_selector(\n \".s-item__price, .price, [class*='price'], [itemprop='price']\"\n )\n link_el = await item.query_selector(\"a\")\n\n title = (await title_el.inner_text()).strip() if title_el else \"\"\n price_text = (await price_el.inner_text()).strip() if price_el else \"\"\n href = await link_el.get_attribute(\"href\") if link_el else \"\"\n\n if not title or len(title) < 5:\n continue\n if href and not href.startswith(\"http\"):\n href = f\"https://{page.url.split('/')[2]}{href}\"\n\n score = calculate_attribute_score(title, keyword.weight)\n if score < 0:\n continue # Accessory Spam filter\n\n _stats[\"total_scanned\"] += 1\n\n # \u2500\u2500 Deduplicate by link \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n if href and db.query(Listing).filter(Listing.link == href).first():\n continue\n\n price = _extract_price(price_text)\n listing = Listing(\n title=title[:500],\n price=price,\n link=href or f\"no-link-{random.randint(0,999999)}\",\n score=score,\n keyword=keyword.term,\n site_name=site.name,\n )\n db.add(listing)\n db.commit()\n new_count += 1\n _stats[\"total_alerts\"] += 1\n\n # \u2500\u2500 Telegram alert \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n alert = (\n f\"\ud83c\udfaf <b>Ghost Node \u2014 New Hit</b>\\n\"\n f\"\ud83d\udce6 {title[:80]}\\n\"\n f\"\ud83d\udcb0 {price_text or 'Price unknown'}\\n\"\n f\"\ud83c\udff7\ufe0f Keyword: <i>{keyword.term}</i> | Score: {score}\\n\"\n f\"\ud83c\udf10 Site: {site.name}\\n\"\n f\"\ud83d\udd17 {href[:200]}\"\n )\n asyncio.create_task(send_telegram(alert))\n\n except Exception as item_exc:\n print(f\"[Scraper] item parse error: {item_exc}\")\n continue\n\n except Exception as nav_exc:\n # Baghdad Optimization \u2014 single site failure never crashes the engine\n print(f\"[Scraper] \u26a0\ufe0f {site.name} | {keyword.term} \u2192 {nav_exc}\")\n\n return new_count\n\n\nasync def nuclear_engine() -> None:\n \"\"\"\n Main scraper loop \u2014 runs forever.\n Pulls a FRESH copy of TargetSites + Config from the DB at the TOP of\n every cycle, so any site/keyword added via the UI is immediately active.\n \"\"\"\n print(\"[Thread B] Nuclear engine igniting\u2026\")\n _stats[\"engine_status\"] = \"Running\"\n\n async with async_playwright() as pw:\n while True:\n if _stats[\"engine_status\"] == \"Paused\":\n await asyncio.sleep(10)\n continue\n\n # \u2500\u2500 Pull live config from DB \u2014 fresh session every cycle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n db = SessionLocal()\n try:\n keywords = db.query(Keyword).all()\n target_sites = db.query(TargetSite).filter(TargetSite.enabled == 1).all()\n timer_val = int(_get_config(\"timer\", \"120\"))\n finally:\n db.close()\n\n # \u2500\u2500 Log exactly which sites the engine will scrape this cycle \u2500\u2500\u2500\u2500\u2500\n if target_sites:\n site_names = \", \".join(f\"'{s.name}'\" for s in target_sites)\n print(f\"[Thread B] \ud83d\udd04 Cycle starting \u2014 {len(target_sites)} site(s): {site_names}\")\n else:\n print(\"[Thread B] \u26a0\ufe0f No enabled TargetSites in DB \u2014 sleeping 60s.\")\n await asyncio.sleep(60)\n continue\n\n if not keywords:\n print(\"[Thread B] \u26a0\ufe0f No Keywords in DB \u2014 sleeping 60s.\")\n await asyncio.sleep(60)\n continue\n\n _stats[\"engine_status\"] = \"Running\"\n cycle_start = datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")\n\n for site in target_sites:\n # \u2500\u2500 New browser context per site (stealth rotation) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n try:\n browser = await pw.chromium.launch(headless=True, args=[\"--no-sandbox\"])\n context = await browser.new_context(\n user_agent=random.choice(_rotating_agents),\n viewport={\"width\": random.choice([1366, 1440, 1920]),\n \"height\": random.choice([768, 900, 1080])},\n locale=\"en-GB\",\n timezone_id=\"Europe/London\",\n )\n\n # Inject playwright-stealth via init script\n await context.add_init_script(\"\"\"\n Object.defineProperty(navigator, 'webdriver', {get: () => undefined});\n Object.defineProperty(navigator, 'plugins', {get: () => [1, 2, 3, 4, 5]});\n window.chrome = { runtime: {} };\n \"\"\")\n\n page = await context.new_page()\n # Block heavy resources to speed up scraping\n await page.route(\"**/*.{png,jpg,jpeg,gif,svg,woff,woff2,ttf,mp4,webp}\",\n lambda route: route.abort())\n\n db = SessionLocal()\n try:\n for kw in keywords:\n found = await scrape_site(page, site, kw, db)\n print(f\"[Scraper] \u2713 {site.name} | '{kw.term}' \u2192 {found} new\")\n # Jitter: 5\u201315 seconds between keywords\n jitter = random.uniform(5, 15)\n await asyncio.sleep(jitter)\n finally:\n db.close()\n\n await browser.close()\n\n except Exception as browser_exc:\n print(f\"[Thread B] Browser error on {site.name}: {browser_exc}\")\n\n _stats[\"last_cycle\"] = cycle_start\n _stats[\"engine_status\"] = \"Idle \u2014 waiting next cycle\"\n print(f\"[Thread B] \u2705 Cycle complete. Sleeping {timer_val}s.\")\n await asyncio.sleep(timer_val)\n\n\ndef run_scraper_thread() -> None:\n loop = asyncio.new_event_loop()\n asyncio.set_event_loop(loop)\n loop.run_until_complete(\n asyncio.gather(nuclear_engine(), telegram_c2_loop())\n )\n\n\n# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Thread A \u2014 FastAPI Dashboard\n# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\napp = FastAPI(title=\"Ghost Node\", version=\"1.0.0\")\n\napp.add_middleware(\n CORSMiddleware,\n allow_origins=[\"*\"],\n allow_methods=[\"*\"],\n allow_headers=[\"*\"],\n)\n\n\n# \u2500\u2500 Static files / Dashboard \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nDASHBOARD_PATH = os.path.join(os.path.dirname(__file__), \"dashboard.html\")\n\n@app.get(\"/\", response_class=HTMLResponse)\nasync def serve_dashboard():\n if os.path.exists(DASHBOARD_PATH):\n with open(DASHBOARD_PATH, \"r\", encoding=\"utf-8\") as f:\n return HTMLResponse(content=f.read())\n return HTMLResponse(\"<h1>Dashboard not found</h1>\", status_code=404)\n\n\n# \u2500\u2500 Stats \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n@app.get(\"/api/stats\")\ndef get_stats():\n uptime = int(time.time() - _stats[\"uptime_start\"])\n return {**_stats, \"uptime_seconds\": uptime}\n\n\n# \u2500\u2500 Listings \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n@app.get(\"/api/listings\")\ndef get_listings(limit: int = 100, db: Session = Depends(get_db)):\n rows = (\n db.query(Listing)\n .order_by(Listing.timestamp.desc())\n .limit(limit)\n .all()\n )\n return [r.to_dict() for r in rows]\n\n\n@app.delete(\"/api/listings/{listing_id}\")\ndef delete_listing(listing_id: int, db: Session = Depends(get_db)):\n row = db.query(Listing).filter(Listing.id == listing_id).first()\n if not row:\n return JSONResponse({\"error\": \"not found\"}, status_code=404)\n db.delete(row)\n db.commit()\n return {\"status\": \"deleted\"}\n\n\n@app.delete(\"/api/listings\")\ndef clear_listings(db: Session = Depends(get_db)):\n db.query(Listing).delete()\n db.commit()\n return {\"status\": \"cleared\"}\n\n\n# \u2500\u2500 Keywords \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n@app.get(\"/api/keywords\")\ndef get_keywords(db: Session = Depends(get_db)):\n return [k.to_dict() for k in db.query(Keyword).all()]\n\n\n@app.post(\"/api/keywords\")\nasync def add_keyword(request: Request, db: Session = Depends(get_db)):\n body = await request.json()\n term = str(body.get(\"term\", \"\")).strip()\n weight = int(body.get(\"weight\", 1))\n if not term:\n return JSONResponse({\"error\": \"term required\"}, status_code=400)\n existing = db.query(Keyword).filter(Keyword.term == term).first()\n if existing:\n return JSONResponse({\"error\": \"duplicate\"}, status_code=409)\n kw = Keyword(term=term, weight=weight)\n db.add(kw)\n db.commit()\n db.refresh(kw)\n return kw.to_dict()\n\n\n@app.delete(\"/api/keywords/{kw_id}\")\ndef delete_keyword(kw_id: int, db: Session = Depends(get_db)):\n row = db.query(Keyword).filter(Keyword.id == kw_id).first()\n if not row:\n return JSONResponse({\"error\": \"not found\"}, status_code=404)\n db.delete(row)\n db.commit()\n return {\"status\": \"deleted\"}\n\n\n# \u2500\u2500 Target Sites \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n@app.get(\"/api/sites\")\ndef get_sites(db: Session = Depends(get_db)):\n return [s.to_dict() for s in db.query(TargetSite).all()]\n\n\n@app.post(\"/api/sites\")\nasync def add_site(request: Request, db: Session = Depends(get_db)):\n \"\"\"\n Registers a new TargetSite.\n Uses request.json() directly (no Pydantic model) to prevent 400 errors.\n Sets enabled=1 explicitly \u2014 never relies on column default under concurrent load.\n Calls db.flush() before db.commit() to force the INSERT into the SQLite\n WAL immediately, so the scraper thread's next DB session sees the new row.\n \"\"\"\n try:\n body = await request.json()\n except Exception:\n return JSONResponse({\"error\": \"invalid JSON body\"}, status_code=400)\n\n name = str(body.get(\"name\", \"\")).strip()\n template = str(body.get(\"url_template\", \"\")).strip()\n selector = str(body.get(\"search_selector\", \"\")).strip()\n\n if not name or not template:\n return JSONResponse({\"error\": \"name and url_template are required\"}, status_code=400)\n if \"{keyword}\" not in template:\n return JSONResponse({\"error\": \"url_template must contain {keyword} placeholder\"}, status_code=400)\n\n site = TargetSite(\n name=name,\n url_template=template,\n search_selector=selector,\n enabled=1, # explicit \u2014 never rely on column default for critical flag\n )\n db.add(site)\n db.flush() # pushes INSERT to SQLite WAL before commit\n db.commit()\n db.refresh(site)\n\n print(f\"[API] \u2705 New TargetSite saved: '{site.name}' id={site.id} enabled={site.enabled}\")\n return site.to_dict()\n\n\n@app.put(\"/api/sites/{site_id}\")\nasync def update_site(site_id: int, request: Request, db: Session = Depends(get_db)):\n body = await request.json()\n row = db.query(TargetSite).filter(TargetSite.id == site_id).first()\n if not row:\n return JSONResponse({\"error\": \"not found\"}, status_code=404)\n for field in (\"name\", \"url_template\", \"search_selector\", \"enabled\"):\n if field in body:\n setattr(row, field, body[field])\n db.commit()\n db.refresh(row)\n return row.to_dict()\n\n\n@app.delete(\"/api/sites/{site_id}\")\ndef delete_site(site_id: int, db: Session = Depends(get_db)):\n row = db.query(TargetSite).filter(TargetSite.id == site_id).first()\n if not row:\n return JSONResponse({\"error\": \"not found\"}, status_code=404)\n db.delete(row)\n db.commit()\n return {\"status\": \"deleted\"}\n\n\n# \u2500\u2500 Config / Settings \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n@app.get(\"/api/config\")\ndef get_config(db: Session = Depends(get_db)):\n rows = db.query(Config).all()\n return {r.key: r.value for r in rows}\n\n\n@app.post(\"/api/config\")\nasync def save_config(request: Request, db: Session = Depends(get_db)):\n \"\"\"\n Accepts a JSON dict of key\u2192value pairs and upserts into the Config table.\n Uses request.json() directly to avoid Pydantic 400 errors.\n db.flush() forces the UPDATEs/INSERTs into the SQLite WAL before commit,\n ensuring the scraper thread's next _get_config() call sees fresh values.\n \"\"\"\n try:\n body = await request.json()\n except Exception:\n return JSONResponse({\"error\": \"invalid JSON body\"}, status_code=400)\n\n saved_keys: list[str] = []\n for key, value in body.items():\n row = db.query(Config).filter(Config.key == key).first()\n if row:\n row.value = str(value)\n else:\n db.add(Config(key=key, value=str(value)))\n saved_keys.append(key)\n\n db.flush() # push dirty rows to SQLite WAL\n db.commit() # finalise the transaction on disk\n\n # Terminal confirmation \u2014 proves the write happened\n print(f\"[API] \u2705 Config saved to DB: {saved_keys}\")\n for k in saved_keys:\n row = db.query(Config).filter(Config.key == k).first()\n display = row.value[:6] + \"\u2026\" if row and row.value and len(row.value) > 6 else (row.value if row else \"\")\n print(f\" {k} = {display!r}\")\n\n return {\"status\": \"saved\", \"keys\": saved_keys}\n\n\n# \u2500\u2500 Engine Control \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n@app.post(\"/api/engine/pause\")\ndef engine_pause():\n _stats[\"engine_status\"] = \"Paused\"\n return {\"status\": \"paused\"}\n\n\n@app.post(\"/api/engine/resume\")\ndef engine_resume():\n _stats[\"engine_status\"] = \"Running\"\n return {\"status\": \"running\"}\n\n\n# \u2500\u2500 Telegram connectivity test \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n@app.post(\"/api/telegram/test\")\nasync def test_telegram():\n \"\"\"\n Sends a test message using whatever token/chat_id is currently in the DB.\n Returns the full Telegram response body so you can diagnose 401/404 etc.\n \"\"\"\n token = _get_config(\"telegram_token\")\n chat_id = _get_config(\"telegram_chat_id\")\n\n if not token or not chat_id:\n return JSONResponse(\n {\"ok\": False, \"error\": \"No token or chat_id saved in DB. Open Settings tab and save first.\"},\n status_code=400,\n )\n\n url = f\"https://api.telegram.org/bot{token}/sendMessage\"\n try:\n async with httpx.AsyncClient(timeout=15) as client:\n r = await client.post(\n url,\n data={\"chat_id\": chat_id, \"text\": \"\ud83d\udc7b Ghost Node \u2014 Telegram test OK!\", \"parse_mode\": \"HTML\"},\n )\n body = r.json()\n if r.status_code == 200:\n return {\"ok\": True, \"telegram_response\": body}\n else:\n return JSONResponse(\n {\"ok\": False, \"http_status\": r.status_code, \"telegram_response\": body},\n status_code=200, # return 200 to JS \u2014 the Telegram error is in the body\n )\n except Exception as exc:\n return JSONResponse({\"ok\": False, \"error\": str(exc)}, status_code=500)\n\n\n# \u2500\u2500 DB read-back diagnostic \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n@app.get(\"/api/debug/db\")\ndef debug_db(db: Session = Depends(get_db)):\n \"\"\"\n Returns the exact contents of Config and TargetSite tables.\n Use this to confirm that Settings-tab saves and new sites\n are genuinely written to sniper.db.\n \"\"\"\n configs = {r.key: r.value for r in db.query(Config).all()}\n # Mask token for security \u2014 show only first 8 chars\n if \"telegram_token\" in configs and configs[\"telegram_token\"]:\n t = configs[\"telegram_token\"]\n configs[\"telegram_token\"] = t[:8] + \"\u2026\" if len(t) > 8 else t\n sites = [s.to_dict() for s in db.query(TargetSite).all()]\n keywords = [k.to_dict() for k in db.query(Keyword).all()]\n return {\n \"config\": configs,\n \"sites\": sites,\n \"keywords\": keywords,\n \"listing_count\": db.query(Listing).count(),\n }\n\n\n# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Entry Point \u2014 spin up threads\n# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nif __name__ == \"__main__\":\n # Thread B + C (scraper & Telegram C2 share the same asyncio event loop)\n scraper_thread = threading.Thread(\n target=run_scraper_thread,\n name=\"GhostNode-Scraper\",\n daemon=True,\n )\n scraper_thread.start()\n print(\"[GhostNode] \ud83d\udd75\ufe0f Ghost Node online \u2014 Dashboard \u2192 http://localhost:8000\")\n\n # Thread A (FastAPI via uvicorn \u2014 blocks main thread)\n uvicorn.run(app, host=\"0.0.0.0\", port=8000, log_level=\"warning\")\n", "created_at": "2026-03-06T22:05:46.747043+00:00"}, {"uuid": "c5dafafa-f6bd-479a-a61e-afbf2031e394", "filename": "models.py", "content": "\"\"\"\nGhost Node \u2014 ORM Models, Heuristic Scoring & DB Seeder\n\"\"\"\n\nfrom __future__ import annotations\n\nimport re\nfrom datetime import datetime\nfrom typing import Optional\n\nfrom sqlalchemy import Column, DateTime, Float, Integer, String, Text\nfrom sqlalchemy.sql import func\n\nfrom database import Base, SessionLocal, engine\n\n# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# ORM Models\n# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nclass Listing(Base):\n __tablename__ = \"listings\"\n\n id = Column(Integer, primary_key=True, index=True)\n title = Column(String(500), nullable=False)\n price = Column(Float, nullable=True)\n link = Column(String(1000), nullable=False, unique=True)\n score = Column(Integer, default=0)\n keyword = Column(String(200), nullable=True)\n site_name = Column(String(200), nullable=True)\n timestamp = Column(DateTime(timezone=True), server_default=func.now())\n\n def to_dict(self) -> dict:\n return {\n \"id\": self.id,\n \"title\": self.title,\n \"price\": self.price,\n \"link\": self.link,\n \"score\": self.score,\n \"keyword\": self.keyword,\n \"site_name\": self.site_name,\n \"timestamp\": self.timestamp.isoformat() if self.timestamp else None,\n }\n\n\nclass Keyword(Base):\n __tablename__ = \"keywords\"\n\n id = Column(Integer, primary_key=True, index=True)\n term = Column(String(200), nullable=False, unique=True)\n weight = Column(Integer, default=1) # multiplier for scoring\n\n def to_dict(self) -> dict:\n return {\"id\": self.id, \"term\": self.term, \"weight\": self.weight}\n\n\nclass Config(Base):\n __tablename__ = \"config\"\n\n id = Column(Integer, primary_key=True, index=True)\n key = Column(String(100), nullable=False, unique=True)\n value = Column(Text, nullable=True)\n\n def to_dict(self) -> dict:\n return {\"id\": self.id, \"key\": self.key, \"value\": self.value}\n\n\nclass TargetSite(Base):\n __tablename__ = \"target_sites\"\n\n id = Column(Integer, primary_key=True, index=True)\n name = Column(String(200), nullable=False)\n url_template = Column(String(1000), nullable=False)\n # e.g. \"#gh-ac\" for eBay \u2014 blank means use url_template directly\n search_selector = Column(String(200), nullable=True, default=\"\")\n enabled = Column(Integer, default=1) # 1 = active, 0 = disabled\n\n def to_dict(self) -> dict:\n return {\n \"id\": self.id,\n \"name\": self.name,\n \"url_template\": self.url_template,\n \"search_selector\": self.search_selector,\n \"enabled\": bool(self.enabled),\n }\n\n\n# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Heuristic Scoring Engine\n# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nPOSITIVE_SIGNALS: list[tuple[str, int]] = [\n (\"GB\", 10),\n (\"RAM\", 10),\n (\"Unlocked\", 10),\n (\"SSD\", 10),\n (\"RTX\", 10),\n (\"GPU\", 10),\n (\"S10\", 10),\n (\"NVMe\", 8),\n (\"OLED\", 8),\n (\"5G\", 6),\n (\"New\", 5),\n (\"Sealed\", 5),\n]\n\nNEGATIVE_SIGNALS: list[tuple[str, int]] = [\n (\"Cover\", -10),\n (\"Case\", -10),\n (\"Sleeve\", -10),\n (\"Box Only\", -10),\n (\"Broken\", -10),\n (\"For Parts\", -10),\n (\"Cracked\", -8),\n (\"Damaged\", -8),\n (\"Read\", -5),\n (\"Faulty\", -10),\n]\n\n\ndef calculate_attribute_score(text: str, keyword_weight: int = 1) -> int:\n \"\"\"\n Scan the listing title for positive / negative signals.\n Returns the weighted heuristic score.\n Only listings with score >= 0 should trigger alerts.\n \"\"\"\n score = 0\n text_upper = text.upper()\n\n for token, delta in POSITIVE_SIGNALS:\n if token.upper() in text_upper:\n score += delta\n\n for token, delta in NEGATIVE_SIGNALS:\n if token.upper() in text_upper:\n score += delta # delta is already negative\n\n # Apply the keyword-level weight multiplier (clamped to \u00b11 to avoid explosion)\n score = score * max(1, keyword_weight)\n return score\n\n\n# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Database Seeder (runs only when tables are brand-new / empty)\n# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nSEED_KEYWORDS = [\n (\"RTX 4090\", 5),\n (\"RTX 4080\", 4),\n (\"RTX 3090\", 4),\n (\"Samsung Tab S10\", 4),\n (\"Samsung Tab S9\", 3),\n (\"iPhone 15 Pro\", 3),\n (\"MacBook Pro M3\", 3),\n (\"Steam Deck\", 3),\n (\"PS5\", 3),\n (\"Xbox Series X\", 3),\n (\"AirPods Pro\", 2),\n (\"DJI Mini 4\", 2),\n]\n\nSEED_SITES = [\n {\n # Mode A \u2014 Direct template: {keyword} is substituted into the URL.\n \"name\": \"eBay UK\",\n \"url_template\": \"https://www.ebay.co.uk/sch/i.html?_nkw={keyword}&_sop=10\",\n \"search_selector\": \"#gh-ac\",\n },\n {\n # Mode A \u2014 Direct template.\n \"name\": \"eBay US\",\n \"url_template\": \"https://www.ebay.com/sch/i.html?_nkw={keyword}&_sop=10\",\n \"search_selector\": \"#gh-ac\",\n },\n {\n # Mode B \u2014 Homepage search: NO {keyword} in url_template.\n # The bot navigates to this URL then locates the search box via\n # search_selector and types the keyword as a human would.\n # ShopGoodwill uses a standard <input id=\"st\"> search field.\n \"name\": \"ShopGoodwill\",\n \"url_template\": \"https://shopgoodwill.com/home\",\n \"search_selector\": \"input#st\",\n },\n]\n\nSEED_CONFIG = [\n (\"telegram_token\", \"\"),\n (\"telegram_chat_id\", \"\"),\n (\"timer\", \"120\"), # seconds between full scrape cycles\n]\n\n\ndef _migrate_schema() -> None:\n \"\"\"\n Safe, idempotent schema migration for users upgrading from an older\n sniper.db that pre-dates the search_selector column.\n SQLite does not support IF NOT EXISTS on ALTER TABLE, so we probe\n the pragma first and only issue the ALTER if the column is absent.\n \"\"\"\n import sqlite3\n # Resolve the actual file path from the engine URL\n db_path = engine.url.database # e.g. \"./sniper.db\" or \"/abs/path/sniper.db\"\n if not db_path or db_path == \":memory:\":\n return\n try:\n conn = sqlite3.connect(db_path)\n cur = conn.cursor()\n cur.execute(\"PRAGMA table_info(target_sites)\")\n cols = {row[1] for row in cur.fetchall()}\n if \"search_selector\" not in cols:\n cur.execute(\"ALTER TABLE target_sites ADD COLUMN search_selector TEXT DEFAULT ''\")\n conn.commit()\n print(\"[GhostNode] \ud83d\udd27 Migrated target_sites: added search_selector column.\")\n conn.close()\n except Exception as exc:\n print(f\"[GhostNode] \u26a0\ufe0f Schema migration skipped: {exc}\")\n\n\ndef seed_database() -> None:\n \"\"\"\n Create all tables (idempotent), run any pending column migrations,\n then populate with seed data on the very first run.\n \"\"\"\n Base.metadata.create_all(bind=engine)\n _migrate_schema() # safe no-op if column already exists\n\n db = SessionLocal()\n try:\n if db.query(Keyword).count() == 0:\n for term, weight in SEED_KEYWORDS:\n db.add(Keyword(term=term, weight=weight))\n\n if db.query(TargetSite).count() == 0:\n for site in SEED_SITES:\n db.add(TargetSite(**site, enabled=1))\n\n if db.query(Config).count() == 0:\n for key, value in SEED_CONFIG:\n db.add(Config(key=key, value=value))\n\n db.commit()\n print(\"[GhostNode] \u2705 Database seeded successfully.\")\n except Exception as exc:\n db.rollback()\n print(f\"[GhostNode] \u26a0\ufe0f Seed error: {exc}\")\n finally:\n db.close()\n", "created_at": "2026-03-06T22:25:14.984405+00:00"}]}, {"uuid": "019ccf78-0724-71ae-a89f-5a9d1e1e3d11", "name": "How to use Claude", "description": "An example project that also doubles as a how-to guide for using Claude. Chat with it to learn more about how to get the most out of chatting with Claude!", "is_private": false, "is_starter_project": true, "prompt_template": "", "created_at": "2026-03-08T22:01:15.788190+00:00", "updated_at": "2026-03-08T22:01:15.788190+00:00", "creator": {"uuid": "46ff33bf-4f36-4557-bbf6-52b84de5893d", "full_name": "Abbas"}, "docs": [{"uuid": "141602c5-25ee-4a8b-abdd-17b070485c14", "filename": "Claude prompting guide.md", "content": "\n# Claude prompting guide\n\n## General tips for effective prompting\n\n### 1. Be clear and specific\n - Clearly state your task or question at the beginning of your message.\n - Provide context and details to help Claude understand your needs.\n - Break complex tasks into smaller, manageable steps.\n\n Bad prompt:\n <prompt>\n \"Help me with a presentation.\"\n </prompt>\n\n Good prompt:\n <prompt>\n \"I need help creating a 10-slide presentation for our quarterly sales meeting. The presentation should cover our Q2 sales performance, top-selling products, and sales targets for Q3. Please provide an outline with key points for each slide.\"\n </prompt>\n\n Why it's better: The good prompt provides specific details about the task, including the number of slides, the purpose of the presentation, and the key topics to be covered.\n\n### 2. Use examples\n - Provide examples of the kind of output you're looking for.\n - If you want a specific format or style, show Claude an example.\n\n Bad prompt:\n <prompt>\n \"Write a professional email.\"\n </prompt>\n\n Good prompt:\n <prompt>\n \"I need to write a professional email to a client about a project delay. Here's a similar email I've sent before:\n\n 'Dear [Client],\n I hope this email finds you well. I wanted to update you on the progress of [Project Name]. Unfortunately, we've encountered an unexpected issue that will delay our completion date by approximately two weeks. We're working diligently to resolve this and will keep you updated on our progress.\n Please let me know if you have any questions or concerns.\n Best regards,\n [Your Name]'\n\n Help me draft a new email following a similar tone and structure, but for our current situation where we're delayed by a month due to supply chain issues.\"\n </prompt>\n\n Why it's better: The good prompt provides a concrete example of the desired style and tone, giving Claude a clear reference point for the new email.\n\n### 3. Encourage thinking\n - For complex tasks, ask Claude to \"think step-by-step\" or \"explain your reasoning.\"\n - This can lead to more accurate and detailed responses.\n\n Bad prompt:\n <prompt>\n \"How can I improve team productivity?\"\n </prompt>\n\n Good prompt:\n <prompt>\n \"I'm looking to improve my team's productivity. Think through this step-by-step, considering the following factors:\n 1. Current productivity blockers (e.g., too many meetings, unclear priorities)\n 2. Potential solutions (e.g., time management techniques, project management tools)\n 3. Implementation challenges\n 4. Methods to measure improvement\n\n For each step, please provide a brief explanation of your reasoning. Then summarize your ideas at the end.\"\n </prompt>\n\n Why it's better: The good prompt asks Claude to think through the problem systematically, providing a guided structure for the response and asking for explanations of the reasoning process. It also prompts Claude to create a summary at the end for easier reading.\n\n### 4. Iterative refinement\n - If Claude's first response isn't quite right, ask for clarifications or modifications.\n - You can always say \"That's close, but can you adjust X to be more like Y?\"\n\n Bad prompt:\n <prompt>\n \"Make it better.\"\n </prompt>\n\n Good prompt:\n <prompt>\n \"That\u2019s a good start, but please refine it further. Make the following adjustments:\n 1. Make the tone more casual and friendly\n 2. Add a specific example of how our product has helped a customer\n 3. Shorten the second paragraph to focus more on the benefits rather than the features\"\n </prompt>\n\n Why it's better: The good prompt provides specific feedback and clear instructions for improvements, allowing Claude to make targeted adjustments instead of just relying on Claude\u2019s innate sense of what \u201cbetter\u201d might be \u2014 which is likely different from the user\u2019s definition!\n\n### 5. Leverage Claude's knowledge\n - Claude has broad knowledge across many fields. Don't hesitate to ask for explanations or background information\n - Be sure to include relevant context and details so that Claude\u2019s response is maximally targeted to be helpful\n\n Bad prompt:\n <prompt>\n \"What is marketing? How do I do it?\"\n </prompt>\n\n Good prompt:\n <prompt>\n \"I'm developing a marketing strategy for a new eco-friendly cleaning product line. Can you provide an overview of current trends in green marketing? Please include:\n 1. Key messaging strategies that resonate with environmentally conscious consumers\n 2. Effective channels for reaching this audience\n 3. Examples of successful green marketing campaigns from the past year\n 4. Potential pitfalls to avoid (e.g., greenwashing accusations)\n\n This information will help me shape our marketing approach.\"\n </prompt>\n\n Why it's better: The good prompt asks for specific, contextually relevant information that leverages Claude's broad knowledge base. It provides context for how the information will be used, which helps Claude frame its answer in the most relevant way.\n\n### 6. Use role-playing\n - Ask Claude to adopt a specific role or perspective when responding.\n\n Bad prompt:\n <prompt>\n \"Help me prepare for a negotiation.\"\n </prompt>\n\n Good prompt:\n <prompt>\n \"You are a fabric supplier for my backpack manufacturing company. I'm preparing for a negotiation with this supplier to reduce prices by 10%. As the supplier, please provide:\n 1. Three potential objections to our request for a price reduction\n 2. For each objection, suggest a counterargument from my perspective\n 3. Two alternative proposals the supplier might offer instead of a straight price cut\n\n Then, switch roles and provide advice on how I, as the buyer, can best approach this negotiation to achieve our goal.\"\n </prompt>\n\n Why it's better: This prompt uses role-playing to explore multiple perspectives of the negotiation, providing a more comprehensive preparation. Role-playing also encourages Claude to more readily adopt the nuances of specific perspectives, increasing the intelligence and performance of Claude\u2019s response.\n\n\n## Task-specific tips and examples\n\n### Content Creation\n\n1. **Specify your audience**\n - Tell Claude who the content is for.\n\n Bad prompt:\n <prompt>\n \"Write something about cybersecurity.\"\n </prompt>\n\n Good prompt:\n <prompt>\n \"I need to write a blog post about cybersecurity best practices for small business owners. The audience is not very tech-savvy, so the content should be:\n 1. Easy to understand, avoiding technical jargon where possible\n 2. Practical, with actionable tips they can implement quickly\n 3. Engaging and slightly humorous to keep their interest\n\n Please provide an outline for a 1000-word blog post that covers the top 5 cybersecurity practices these business owners should adopt.\"\n </prompt>\n\n Why it's better: The good prompt specifies the audience, desired tone, and key characteristics of the content, giving Claude clear guidelines for creating appropriate and effective output.\n\n2. **Define the tone and style**\n - Describe the desired tone.\n - If you have a style guide, mention key points from it.\n\n Bad prompt:\n <prompt>\n \"Write a product description.\"\n </prompt>\n\n Good prompt:\n <prompt>\n \"Please help me write a product description for our new ergonomic office chair. Use a professional but engaging tone. Our brand voice is friendly, innovative, and health-conscious. The description should:\n 1. Highlight the chair's key ergonomic features\n 2. Explain how these features benefit the user's health and productivity\n 3. Include a brief mention of the sustainable materials used\n 4. End with a call-to-action encouraging readers to try the chair\n\n Aim for about 200 words.\"\n </prompt>\n\n Why it's better: This prompt provides clear guidance on the tone, style, and specific elements to include in the product description.\n\n3. **Define output structure**\n - Provide a basic outline or list of points you want covered.\n\n Bad prompt:\n <prompt>\n \"Create a presentation on our company results.\"\n </prompt>\n\n Good prompt:\n <prompt>\n \"I need to create a presentation on our Q2 results. Structure this with the following sections:\n 1. Overview\n 2. Sales Performance\n 3. Customer Acquisition\n 4. Challenges\n 5. Q3 Outlook\n\n For each section, suggest 3-4 key points to cover, based on typical business presentations. Also, recommend one type of data visualization (e.g., graph, chart) that would be effective for each section.\"\n </prompt>\n\n Why it's better: This prompt provides a clear structure and asks for specific elements (key points and data visualizations) for each section.\n\n### Document summary and Q&A\n\n1. **Be specific about what you want**\n - Ask for a summary of specific aspects or sections of the document.\n - Frame your questions clearly and directly.\n - Be sure to specify what kind of summary (output structure, content type) you want\n\n2. **Use the document names**\n - Refer to attached documents by name.\n\n3. **Ask for citations**\n - Request that Claude cites specific parts of the document in its answers.\n\nHere is an example that combines all three of the above techniques:\n\n Bad prompt:\n <prompt>\n \"Summarize this report for me.\"\n </prompt>\n\n Good prompt:\n <prompt>\n \"I've attached a 50-page market research report called 'Tech Industry Trends 2023'. Can you provide a 2-paragraph summary focusing on AI and machine learning trends? Then, please answer these questions:\n 1. What are the top 3 AI applications in business for this year?\n 2. How is machine learning impacting job roles in the tech industry?\n 3. What potential risks or challenges does the report mention regarding AI adoption?\n\n Please cite specific sections or page numbers when answering these questions.\"\n </prompt>\n\n Why it's better: This prompt specifies the exact focus of the summary, provides specific questions, and asks for citations, ensuring a more targeted and useful response. It also indicates the ideal summary output structure, such as limiting the response to 2 paragraphs.\n\n### Data analysis and visualization\n\n1. **Specify the desired format**\n - Clearly describe the format you want the data in.\n\n Bad prompt:\n <prompt>\n \"Analyze our sales data.\"\n </prompt>\n\n Good prompt:\n <prompt>\n \"I've attached a spreadsheet called 'Sales Data 2023'. Can you analyze this data and present the key findings in the following format:\n\n 1. Executive Summary (2-3 sentences)\n\n 2. Key Metrics:\n - Total sales for each quarter\n - Top-performing product category\n - Highest growth region\n\n 3. Trends:\n - List 3 notable trends, each with a brief explanation\n\n 4. Recommendations:\n - Provide 3 data-driven recommendations, each with a brief rationale\n\n After the analysis, suggest three types of data visualizations that would effectively communicate these findings.\"\n </prompt>\n\n Why it's better: This prompt provides a clear structure for the analysis, specifies key metrics to focus on, and asks for recommendations and visualization suggestions for further formatting.\n\n### Brainstorming\n 1. Use Claude to generate ideas by asking for a list of possibilities or alternatives.\n - Be specific about what topics you want Claude to cover in its brainstorming\n\n Bad prompt:\n <prompt>\n \"Give me some team-building ideas.\"\n </prompt>\n\n Good prompt:\n <prompt>\n \"We need to come up with team-building activities for our remote team of 20 people. Can you help me brainstorm by:\n 1. Suggesting 10 virtual team-building activities that promote collaboration\n 2. For each activity, briefly explain how it fosters teamwork\n 3. Indicate which activities are best for:\n a) Ice-breakers\n b) Improving communication\n c) Problem-solving skills\n 4. Suggest one low-cost option and one premium option.\"\n </prompt>\n\n Why it's better: This prompt provides specific parameters for the brainstorming session, including the number of ideas, type of activities, and additional categorization, resulting in a more structured and useful output.\n\n2. Request responses in specific formats like bullet points, numbered lists, or tables for easier reading.\n\n Bad Prompt:\n <prompt>\n \"Compare project management software options.\"\n </prompt>\n\n Good Prompt:\n <prompt>\n \"We're considering three different project management software options: Asana, Trello, and Microsoft Project. Can you compare these in a table format using the following criteria:\n 1. Key Features\n 2. Ease of Use\n 3. Scalability\n 4. Pricing (include specific plans if possible)\n 5. Integration capabilities\n 6. Best suited for (e.g., small teams, enterprise, specific industries)\"\n </prompt>\n\n Why it's better: This prompt requests a specific structure (table) for the comparison, provides clear criteria, making the information easy to understand and apply.\n\n## Troubleshooting, minimizing hallucinations, and maximizing performance\n\n1. **Allow Claude to acknowledge uncertainty**\n - Tell Claude that it should say it doesn\u2019t know if it doesn\u2019t know. Ex. \u201cIf you're unsure about something, it's okay to admit it. Just say you don\u2019t know.\u201d\n\n2. **Break down complex tasks**\n - If a task seems too large and Claude is missing steps or not performing certain steps well, break it into smaller steps and work through them with Claude one message at a time.\n\n3. **Include all contextual information for new requests**\n - Claude doesn't retain information from previous conversations, so include all necessary context in each new conversation.\n\n## Example good vs. bad prompt examples\n\nThese are more examples that combine multiple prompting techniques to showcase the stark difference between ineffective and highly effective prompts.\n\n### Example 1: Marketing strategy development\n\nBad prompt:\n<prompt>\n\"Help me create a marketing strategy.\"\n</prompt>\n\nGood prompt:\n<prompt>\n\"As a senior marketing consultant, I need your help developing a comprehensive marketing strategy for our new eco-friendly smartphone accessory line. Our target audience is environmentally conscious millennials and Gen Z consumers. Please provide a detailed strategy that includes:\n\n1. Market Analysis:\n - Current trends in eco-friendly tech accessories\n - 2-3 key competitors and their strategies\n - Potential market size and growth projections\n\n2. Target Audience Persona:\n - Detailed description of our ideal customer\n - Their pain points and how our products solve them\n\n3. Marketing Mix:\n - Product: Key features to highlight\n - Price: Suggested pricing strategy with rationale\n - Place: Recommended distribution channels\n - Promotion: \n a) 5 marketing channels to focus on, with pros and cons for each\n b) 3 creative campaign ideas for launch\n\n4. Content Strategy:\n - 5 content themes that would resonate with our audience\n - Suggested content types (e.g., blog posts, videos, infographics)\n\n5. KPIs and Measurement:\n - 5 key metrics to track\n - Suggested tools for measuring these metrics\n\nPlease present this information in a structured format with headings and bullet points. Where relevant, explain your reasoning or provide brief examples.\n\nAfter outlining the strategy, please identify any potential challenges or risks we should be aware of, and suggest mitigation strategies for each.\"\n</prompt>\n\nWhy it's better: This prompt combines multiple techniques including role assignment, specific task breakdown, structured output request, brainstorming (for campaign ideas and content themes), and asking for explanations. It provides clear guidelines while allowing room for Claude's analysis and creativity.\n\n### Example 2: Financial report analysis\n\nBad prompt:\n<prompt>\n\"Analyze this financial report.\"\n</prompt>\n\nGood prompt:\n<prompt>\n\"I've attached our company's Q2 financial report titled 'Q2_2023_Financial_Report.pdf'. Act as a seasoned CFO and analyze this report and prepare a briefing for our board of directors. Please structure your analysis as follows:\n\n1. Executive Summary (3-4 sentences highlighting key points)\n\n2. Financial Performance Overview:\n a) Revenue: Compare to previous quarter and same quarter last year\n b) Profit margins: Gross and Net, with explanations for any significant changes\n c) Cash flow: Highlight any concerns or positive developments\n\n3. Key Performance Indicators:\n - List our top 5 KPIs and their current status (Use a table format)\n - For each KPI, provide a brief explanation of its significance and any notable trends\n\n4. Segment Analysis:\n - Break down performance by our three main business segments\n - Identify the best and worst performing segments, with potential reasons for their performance\n\n5. Balance Sheet Review:\n - Highlight any significant changes in assets, liabilities, or equity\n - Calculate and interpret key ratios (e.g., current ratio, debt-to-equity)\n\n6. Forward-Looking Statements:\n - Based on this data, provide 3 key predictions for Q3\n - Suggest 2-3 strategic moves we should consider to improve our financial position\n\n7. Risk Assessment:\n - Identify 3 potential financial risks based on this report\n - Propose mitigation strategies for each risk\n\n8. Peer Comparison:\n - Compare our performance to 2-3 key competitors (use publicly available data)\n - Highlight areas where we're outperforming and areas for improvement\n\nPlease use charts or tables where appropriate to visualize data. For any assumptions or interpretations you make, please clearly state them and provide your reasoning.\n\nAfter completing the analysis, please generate 5 potential questions that board members might ask about this report, along with suggested responses.\n\nFinally, summarize this entire analysis into a single paragraph that I can use as an opening statement in the board meeting.\"\n</prompt>\n\nWhy it's better: This prompt combines role-playing (as CFO), structured output, specific data analysis requests, predictive analysis, risk assessment, comparative analysis, and even anticipates follow-up questions. It provides a clear framework while encouraging deep analysis and strategic thinking.\n", "created_at": "2026-03-08T22:01:15.788190+00:00"}]}] |