commit 4477d56b30bbd6334c1ea395312f2010af58ffee Author: drjones Date: Sun Aug 2 22:36:59 2026 -0700 ccsite-manager: fleet orchestrator, BTCPay auto-settlement, watchdog, dashboard diff --git a/__pycache__/dashboard.cpython-311.pyc b/__pycache__/dashboard.cpython-311.pyc new file mode 100644 index 0000000..bb91dad Binary files /dev/null and b/__pycache__/dashboard.cpython-311.pyc differ diff --git a/__pycache__/watchdog.cpython-311.pyc b/__pycache__/watchdog.cpython-311.pyc new file mode 100644 index 0000000..541e02b Binary files /dev/null and b/__pycache__/watchdog.cpython-311.pyc differ diff --git a/btc_chain.db b/btc_chain.db new file mode 100644 index 0000000..03b48aa Binary files /dev/null and b/btc_chain.db differ diff --git a/btc_monitor.py b/btc_monitor.py new file mode 100644 index 0000000..cf77cdc --- /dev/null +++ b/btc_monitor.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +""" +BTC Blockchain Monitor โ€” polls blockstream.info for real transactions. +Stores all TXs in SQLite. Detects new deposits. Feeds the dashboard. +""" +import requests +import sqlite3 +import time +import json +from datetime import datetime + +BTC_ADDRESS = "35vTSjPCaqudcL5cj5ChpNDmqrpPFmEnss" +DB_PATH = "/Users/drjones/ccsite-manager/btc_chain.db" +BLOCKSTREAM_URL = f"https://blockstream.info/api/address/{BTC_ADDRESS}/txs" + +def init_db(): + conn = sqlite3.connect(DB_PATH) + c = conn.cursor() + c.execute('''CREATE TABLE IF NOT EXISTS transactions ( + txid TEXT PRIMARY KEY, + timestamp INTEGER, + amount_btc REAL, + amount_sats INTEGER, + confirmations INTEGER, + fee INTEGER, + raw_json TEXT, + first_seen REAL, + alerted INTEGER DEFAULT 0 + )''') + c.execute('CREATE INDEX IF NOT EXISTS idx_tx_time ON transactions(timestamp)') + conn.commit() + conn.close() + +def check_blockchain(): + """Poll Blockstream API for transactions. Returns list of new TXs.""" + try: + r = requests.get(BLOCKSTREAM_URL, timeout=15) + if r.status_code != 200: + return [] + txs = r.json() + except Exception as e: + print(f"Blockstream API error: {e}") + return [] + + conn = sqlite3.connect(DB_PATH) + c = conn.cursor() + new_txs = [] + + for tx in txs: + txid = tx['txid'] + # Check if we've already seen this TX + c.execute("SELECT txid FROM transactions WHERE txid = ?", (txid,)) + if c.fetchone(): + continue + + # Calculate BTC amount sent TO our address (only count vouts where our address receives) + total_out = 0 + for vout in tx.get('vout', []): + addr = vout.get('scriptpubkey_address', '') + if addr == BTC_ADDRESS: + total_out += vout.get('value', 0) + + if total_out == 0: + continue # Skip transactions where our address isn't receiving + + # Get fee and confirmations + fee = tx.get('fee', 0) + confs = tx.get('status', {}).get('confirmed', False) + conf_count = tx.get('status', {}).get('block_height', 0) + ts = tx.get('status', {}).get('block_time', int(time.time())) + + c.execute('''INSERT INTO transactions (txid, timestamp, amount_btc, amount_sats, + confirmations, fee, raw_json, first_seen) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)''', + (txid, ts, total_out, int(total_out * 100000000), + conf_count if confs else 0, fee, json.dumps(tx), time.time())) + + new_txs.append({ + 'txid': txid, + 'amount_btc': total_out, + 'amount_usd': round(total_out * 68000, 2), # Approximate BTC price + 'confirmations': conf_count if confs else 0, + 'time': ts, + }) + + conn.commit() + conn.close() + return new_txs + +def auto_complete_orders(new_txs): + """When new TXs arrive, try to match them to pending orders on all 10 sites.""" + SITE_IPS = [f"10.30.20.{ip}" for ip in ['210','211','212','213','214','217','218','219','220','221']] + + for tx in new_txs: + btc_amount = tx['amount_btc'] + if btc_amount <= 0: + continue + + # Try each site + for ip in SITE_IPS: + try: + r = requests.get(f"http://{ip}:5000/api/products", timeout=5) + if r.status_code != 200: + continue + + # Check for pending orders via health endpoint + # We need to tell the site "a payment of X BTC arrived, check pending orders" + r2 = requests.post(f"http://{ip}:5000/api/btc-check", + json={"amount_btc": btc_amount, "txid": tx['txid']}, + timeout=5) + if r2.status_code == 200: + result = r2.json() + if result.get('matched'): + print(f" โœ… Auto-completed order on {ip}: {result.get('order_code','?')}") + except: + pass + +def get_stats(): + """Get aggregate stats from stored transactions.""" + conn = sqlite3.connect(DB_PATH) + c = conn.cursor() + + c.execute("SELECT COUNT(*), COALESCE(SUM(amount_btc),0), COALESCE(SUM(amount_sats),0) FROM transactions") + count, total_btc, total_sats = c.fetchone() + + c.execute("SELECT COUNT(*), COALESCE(SUM(amount_btc),0) FROM transactions WHERE first_seen > ?", + (time.time() - 86400,)) + day_count, day_btc = c.fetchone() + + c.execute("SELECT txid, amount_btc, timestamp FROM transactions ORDER BY timestamp DESC LIMIT 10") + recent = [{'txid': r[0][:16]+'...', 'btc': r[1], 'ts': r[2]} for r in c.fetchall()] + + conn.close() + + return { + 'total_txs': count, + 'total_btc': round(total_btc, 8), + 'total_sats': total_sats, + 'total_usd': round(total_btc * 68000, 2), + 'day_txs': day_count, + 'day_btc': round(day_btc, 8), + 'day_usd': round(day_btc * 68000, 2), + 'recent': recent, + 'address': BTC_ADDRESS, + } + +def monitor_loop(callback=None): + """Run continuously, calling callback with new TXs.""" + init_db() + print(f"โ‚ฟ BTC Monitor started โ€” watching {BTC_ADDRESS[:16]}...") + print(f" Polling blockstream.info every 60s") + + while True: + try: + new = check_blockchain() + if new: + for tx in new: + print(f" ๐Ÿ’ฐ NEW TX: {tx['amount_btc']:.8f} BTC (${tx['amount_usd']:.2f}) โ€” {tx['txid'][:16]}...") + if callback: + callback(new) + else: + # Silent tick โ€” no new TXs + pass + except Exception as e: + print(f"Monitor error: {e}") + time.sleep(60) + +if __name__ == '__main__': + import sys + if len(sys.argv) > 1 and sys.argv[1] == '--once': + new = check_blockchain() + stats = get_stats() + print(json.dumps({'new_txs': len(new), 'stats': stats}, indent=2)) + elif len(sys.argv) > 1 and sys.argv[1] == '--stats': + stats = get_stats() + print(json.dumps(stats, indent=2)) + else: + monitor_loop() diff --git a/ccsite.py b/ccsite.py new file mode 100644 index 0000000..71d325e --- /dev/null +++ b/ccsite.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python3 +""" +ccsite-manager โ€” Mass orchestration CLI for the 10-shop fleet. +Usage: + ccsite push [--site N] Push a file to all/specific sites + ccsite cmd "" [--site N] Run a shell command on all sites + ccsite restart [--site N] Restart the shop service + ccsite sql "" [--site N] Run SQLite query on all sites + ccsite list List all sites with status + ccsite health Health check all sites + ccsite push-template [--site N] Push a Jinja2 template to all sites +""" + +import subprocess +import sys +import json +import base64 +import time +from concurrent.futures import ThreadPoolExecutor, as_completed +from pathlib import Path + +SITES = { + 1: {"ip": "10.30.20.210", "vmid": "123", "name": "Digital Marketplace"}, + 2: {"ip": "10.30.20.211", "vmid": "124", "name": "CardVault Pro"}, + 3: {"ip": "10.30.20.212", "vmid": "125", "name": "StreamPass Hub"}, + 4: {"ip": "10.30.20.213", "vmid": "126", "name": "KeyForge Digital"}, + 5: {"ip": "10.30.20.214", "vmid": "127", "name": "CoinBridge Market"}, + 6: {"ip": "10.30.20.217", "vmid": "128", "name": "GiftShift"}, + 7: {"ip": "10.30.20.218", "vmid": "129", "name": "PremiumPortal"}, + 8: {"ip": "10.30.20.219", "vmid": "130", "name": "DigiDeals"}, + 9: {"ip": "10.30.20.220", "vmid": "131", "name": "NexusKeys"}, + 10: {"ip": "10.30.20.221", "vmid": "132", "name": "VaultPass"}, +} + +PROXMOX = "root@10.30.20.85" +SHOP_DIR = "/opt/shop" +SHOP_DB = "/opt/shop/shop.db" + +def ssh_cmd(ip, cmd, timeout=15): + """Run a command on a site via direct SSH.""" + full = f"ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=5 root@{ip} '{cmd}'" + try: + r = subprocess.run(full, shell=True, capture_output=True, text=True, timeout=timeout) + return {"ok": r.returncode == 0, "stdout": r.stdout.strip(), "stderr": r.stderr.strip()} + except subprocess.TimeoutExpired: + return {"ok": False, "stdout": "", "stderr": "TIMEOUT"} + except Exception as e: + return {"ok": False, "stdout": "", "stderr": str(e)} + +def pct_exec(vmid, cmd, timeout=15): + """Run a command inside a container via Proxmox pct exec.""" + safe_cmd = cmd.replace("'", "'\"'\"'") + full = f"ssh {PROXMOX} \"pct exec {vmid} -- bash -c '{safe_cmd}'\"" + try: + r = subprocess.run(full, shell=True, capture_output=True, text=True, timeout=timeout) + return {"ok": r.returncode == 0, "stdout": r.stdout.strip(), "stderr": r.stderr.strip()} + except subprocess.TimeoutExpired: + return {"ok": False, "stdout": "", "stderr": "TIMEOUT"} + except Exception as e: + return {"ok": False, "stdout": "", "stderr": str(e)} + +def push_file(ip, local_path, remote_path, timeout=15): + """Push a file to a site via base64 + SSH.""" + with open(local_path, 'rb') as f: + b64 = base64.b64encode(f.read()).decode() + cmd = f"echo '{b64}' | base64 -d > {remote_path}" + return ssh_cmd(ip, cmd, timeout) + +def push_content(ip, content, remote_path, timeout=15): + """Push raw content to a site.""" + b64 = base64.b64encode(content.encode()).decode() + cmd = f"echo '{b64}' | base64 -d > {remote_path}" + return ssh_cmd(ip, cmd, timeout) + +def run_on_sites(sites_filter=None, func=None, desc=""): + """Run a function across sites in parallel. Returns dict of results.""" + targets = {k: v for k, v in SITES.items() if sites_filter is None or k in sites_filter} + results = {} + with ThreadPoolExecutor(max_workers=10) as ex: + futures = {ex.submit(func, s): (i, s) for i, s in targets.items()} + for f in as_completed(futures): + i, s = futures[f] + try: + results[i] = f.result() + except Exception as e: + results[i] = {"ok": False, "stderr": str(e)} + status = "โœ“" if results[i].get("ok") else "โœ—" + print(f" [{status}] ccsite{i} ({s['name']}) โ€” {desc}") + return results + +# โ”€โ”€โ”€ Commands โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +def cmd_list(): + print(f"{'#':<3} {'Name':<25} {'IP':<16} {'VMID':<6} Status") + print("-" * 60) + def check(s): + r = ssh_cmd(s['ip'], "systemctl is-active shop 2>/dev/null || echo dead") + status = r.get('stdout', '?').strip() + return r + results = run_on_sites(func=check, desc="status check") + for i in sorted(SITES): + s = SITES[i] + r = results.get(i, {}) + status = r.get('stdout', '?').strip() + status_icon = "๐ŸŸข" if status == "active" else "๐Ÿ”ด" + print(f"{i:<3} {s['name']:<25} {s['ip']:<16} {s['vmid']:<6} {status_icon} {status}") + +def cmd_health(): + print("Health check across all sites...") + def check(s): + try: + r = subprocess.run( + f"curl -s --connect-timeout 3 http://{s['ip']}:5000/health", + shell=True, capture_output=True, text=True, timeout=5 + ) + if r.returncode == 0: + d = json.loads(r.stdout) + return {"ok": True, "stdout": f"{d.get('products', '?')} products, {d.get('users', '?')} users"} + return {"ok": False, "stdout": r.stderr or "no response"} + except: + return {"ok": False, "stdout": "connection failed"} + results = run_on_sites(func=check, desc="health") + ok = sum(1 for r in results.values() if r.get("ok")) + print(f"\n{ok}/10 sites healthy") + +def cmd_restart(sites_filter=None): + print("Restarting shop service...") + def restart(s): + return ssh_cmd(s['ip'], "systemctl restart shop && sleep 1 && systemctl is-active shop") + run_on_sites(sites_filter, restart, "restart") + +def cmd_push(args): + local = args[0] + remote = args[1] if len(args) > 1 else f"{SHOP_DIR}/{Path(local).name}" + sites_filter = parse_site_filter(args) + print(f"Pushing {local} โ†’ {remote}") + def push(s): + return push_file(s['ip'], local, remote) + results = run_on_sites(sites_filter, push, f"โ†’ {remote}") + ok = sum(1 for r in results.values() if r.get("ok")) + print(f"\nPushed to {ok}/{len(results)} sites") + # Restart if pushing to templates or app + if 'templates' in remote or 'app.py' in remote: + print("Code change detected โ€” restarting services...") + cmd_restart(sites_filter) + +def cmd_cmd(args): + if not args: + print("Usage: ccsite cmd '' [--site N]") + return + cmd_str = args[0] + sites_filter = parse_site_filter(args) + print(f"Running: {cmd_str}") + def run(s): + return ssh_cmd(s['ip'], cmd_str, timeout=20) + results = run_on_sites(sites_filter, run, cmd_str[:40]) + for i in sorted(results): + r = results[i] + out = r.get('stdout', '') or r.get('stderr', '') + if out: + print(f"\nโ”€โ”€ ccsite{i} ({SITES[i]['name']}) โ”€โ”€") + print(out[:500]) + +def cmd_sql(args): + if not args: + print("Usage: ccsite sql '' [--site N]") + return + sql = args[0] + sites_filter = parse_site_filter(args) + print(f"SQL: {sql}") + def run(s): + return ssh_cmd(s['ip'], f"sqlite3 {SHOP_DB} \"{sql}\" 2>&1", timeout=10) + results = run_on_sites(sites_filter, run, sql[:40]) + for i in sorted(results): + r = results[i] + out = r.get('stdout', '') or r.get('stderr', '') + print(f"\nโ”€โ”€ ccsite{i} ({SITES[i]['name']}) โ”€โ”€") + print(out[:500] if out else "(no results)") + +def parse_site_filter(args): + """Extract --site N from args. Returns set of site IDs or None for all.""" + for a in args: + if a.startswith('--site='): + try: + return {int(a.split('=')[1])} + except: + pass + return None + +def print_usage(): + print(__doc__) + +# โ”€โ”€โ”€ Main โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +if __name__ == '__main__': + if len(sys.argv) < 2: + print_usage() + sys.exit(1) + + command = sys.argv[1] + rest = sys.argv[2:] + + if command == 'list': + cmd_list() + elif command == 'health': + cmd_health() + elif command == 'restart': + cmd_restart(parse_site_filter(rest)) + elif command == 'push': + cmd_push(rest) + elif command == 'cmd': + cmd_cmd(rest) + elif command == 'sql': + cmd_sql(rest) + else: + print(f"Unknown command: {command}") + print_usage() diff --git a/crypto-casinos.html b/crypto-casinos.html new file mode 100644 index 0000000..cfe8f03 --- /dev/null +++ b/crypto-casinos.html @@ -0,0 +1,175 @@ + + + + + +No-KYC Crypto Casinos โ€” Buy & Play & Withdraw + + + + +

๐ŸŽฐ No-KYC Crypto Casinos

+

Buy crypto on platform โ†’ gamble โ†’ withdraw. No documents. No verification. Listed by feature completeness.

+ +
+ + + + + + + + + + + + + + + +
CasinoNo KYCBuy Crypto
On Platform
Instant
Withdraw
Sports
Betting
VPN
Friendly
TypeNotes
+ +
+

๐Ÿ” Find More โ€” Google Dorks

+
+
+ +
+

๐Ÿ”Ž Search a Specific Domain for Casino Features

+ + +
+
+ + + + diff --git a/dashboard.db b/dashboard.db new file mode 100644 index 0000000..de1b1f4 Binary files /dev/null and b/dashboard.db differ diff --git a/dashboard.py b/dashboard.py new file mode 100644 index 0000000..8eb296b --- /dev/null +++ b/dashboard.py @@ -0,0 +1,646 @@ +#!/usr/bin/env python3 +""" +CCSite Fleet Dashboard โ€” local aggregator running on MacBook. +Polls all 10 shop sites + BTCPay Server and displays real-time stats. +""" +import json +import time +import threading +import sqlite3 +from datetime import datetime, timedelta +from collections import defaultdict +from flask import Flask, render_template, jsonify, request +import requests +import urllib3 +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +# Import watchdog for health monitoring +try: + from watchdog import get_state as get_watchdog_state, run_all_checks as watchdog_scan + WATCHDOG_AVAILABLE = True +except ImportError: + WATCHDOG_AVAILABLE = False + +SITES = { + 1: {"name": "VaultPass", "ip": "10.30.20.210", "emoji": "๐ŸŽฎ"}, + 2: {"name": "StreamPass", "ip": "10.30.20.211", "emoji": "๐ŸŽฌ"}, + 3: {"name": "KeyForge", "ip": "10.30.20.212", "emoji": "๐Ÿ’ป"}, + 4: {"name": "SocialForge", "ip": "10.30.20.213", "emoji": "๐Ÿฆ"}, + 5: {"name": "CardVault", "ip": "10.30.20.214", "emoji": "๐Ÿ’ณ"}, + 6: {"name": "CloudNexus", "ip": "10.30.20.217", "emoji": "โ˜๏ธ"}, + 7: {"name": "LearnForge", "ip": "10.30.20.218", "emoji": "๐Ÿ“š"}, + 8: {"name": "PrivacyPass", "ip": "10.30.20.219", "emoji": "๐Ÿ›ก๏ธ"}, + 9: {"name": "CreatorForge", "ip": "10.30.20.220", "emoji": "๐ŸŽจ"}, + 10: {"name": "FoodVault", "ip": "10.30.20.221", "emoji": "๐Ÿ•"}, +} + +# BTCPay config +BTCPAY_URL = "https://10.30.20.140" +BTCPAY_KEY = "6026288e2e315984661c748baafd509e81a75f22" + +# Per-site BTCPay store IDs +BTCPAY_STORES = { + 1: "AYZWEAt63TgY4QaxwNBawHASMKcimJabcZxSczRqZG81", + 2: "5pi94zmY3F26pTZJ99JfsFG9pb8atbz7fpALrPjS2BRq", + 3: "3mc58myPrkdN7hRFdsBwJxbVgBQQ3VvSYZygKQC3FiSG", + 4: "E1uZ5nmU6qSpMB8sna7fNdxZFiem9JmkxB9LdJkFN23B", + 5: "BcvaUvv8MM8if5dzqe5yLnNERG6EwjjmeMok54ATqjWf", + 6: "GvSsA9xaqymhswsiPo4APHMuT2sXCCtnBLgPWcHigxVx", + 7: "A2TXbpkpj88Wtj34dLS9yzjMgmcodysEaRhTjc3rFcUr", + 8: "9N8dTC99uXn3W3e2F9o8kVQ5ewijh87NevKmMATF1Jcd", + 9: "39BKRF3zQb6Gi8LiKo1US13bzJfhpCzkwoHP6pCyQFwD", + 10: "9vCjKrKbwgey4GH7DBAMa6FgkiApgHvd5khFpWSZzJL2", +} + +DB_PATH = "/Users/drjones/ccsite-manager/dashboard.db" +BTC_ADDRESS = "35vTSjPCaqudcL5cj5ChpNDmqrpPFmEnss" +app = Flask(__name__, static_folder='/Users/drjones/ccsite-manager', static_url_path='') + +def init_db(): + conn = sqlite3.connect(DB_PATH) + c = conn.cursor() + c.execute('''CREATE TABLE IF NOT EXISTS snapshots ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_id INTEGER, + timestamp REAL, + users INTEGER, + products INTEGER, + orders INTEGER, + revenue REAL, + status TEXT, + response_ms REAL + )''') + c.execute('''CREATE TABLE IF NOT EXISTS events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_id INTEGER, + timestamp REAL, + event_type TEXT, + detail TEXT + )''') + c.execute('''CREATE TABLE IF NOT EXISTS btcpay_snapshots ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp REAL, + sync_pct REAL, + blocks INTEGER, + headers INTEGER, + synced INTEGER, + store_count INTEGER, + stores_with_wallet INTEGER, + total_invoices INTEGER + )''') + c.execute('''CREATE TABLE IF NOT EXISTS site_orders ( + site_id INTEGER, order_id INTEGER, product TEXT, amount REAL, qty INTEGER, + status TEXT, email TEXT, code TEXT, date TEXT, ts REAL, + PRIMARY KEY (site_id, order_id) + )''') + c.execute('''CREATE TABLE IF NOT EXISTS site_users ( + site_id INTEGER, user_id INTEGER, username TEXT, email TEXT, membership TEXT, + balance REAL, orders INTEGER, joined TEXT, ts REAL, + PRIMARY KEY (site_id, user_id) + )''') + c.execute('''CREATE TABLE IF NOT EXISTS site_txs ( + site_id INTEGER, tx_id INTEGER, user TEXT, amount REAL, method TEXT, + type TEXT, date TEXT, + PRIMARY KEY (site_id, tx_id) + )''') + c.execute('CREATE INDEX IF NOT EXISTS idx_orders_date ON site_orders(date DESC)') + c.execute('CREATE INDEX IF NOT EXISTS idx_users_joined ON site_users(joined DESC)') + c.execute('CREATE INDEX IF NOT EXISTS idx_snap_site_time ON snapshots(site_id, timestamp)') + c.execute('CREATE INDEX IF NOT EXISTS idx_events_time ON events(timestamp DESC)') + conn.commit() + conn.close() + +# In-memory current state (updated by poller) +state = {"sites": {}, "totals": {}, "events": [], "last_poll": None, "btcpay": {}} +state_lock = threading.Lock() + +def poll_site(site_id, info): + """Poll a single site for stats.""" + try: + t0 = time.time() + r = requests.get(f"http://{info['ip']}:5000/health", timeout=5) + ms = (time.time() - t0) * 1000 + if r.status_code == 200: + data = r.json() + return { + "site_id": site_id, + "name": info["name"], "emoji": info["emoji"], "ip": info["ip"], + "status": "online", "response_ms": round(ms), + "users": data.get("users", 0), + "products": data.get("products", 0), + "site_name": data.get("site", "?"), + "orders": data.get("orders_total", 0), + "orders_today": data.get("orders_today", 0), + "revenue": data.get("revenue_total", 0), + "revenue_today": data.get("revenue_today", 0), + "signups_today": data.get("signups_today", 0), + } + except Exception as e: + return { + "site_id": site_id, "name": info["name"], "emoji": info["emoji"], "ip": info["ip"], + "status": "offline", "response_ms": 0, "users": 0, "products": 0, "site_name": "?", + "orders": 0, "orders_today": 0, "revenue": 0, "revenue_today": 0, "signups_today": 0, + "error": str(e)[:100], + } + +def poll_site_details(site_id, info): + """Fetch /api/dashboard-data from a shop and upsert orders/users/txs. + Returns per-site detail summary or None on failure.""" + try: + r = requests.get(f"http://{info['ip']}:5000/api/dashboard-data", timeout=8) + if r.status_code != 200: + return None + data = r.json() + now = time.time() + conn = sqlite3.connect(DB_PATH) + c = conn.cursor() + for o in data.get("orders", []): + c.execute("""INSERT OR REPLACE INTO site_orders + (site_id, order_id, product, amount, qty, status, email, code, date, ts) + VALUES (?,?,?,?,?,?,?,?,?,?)""", + (site_id, o.get("id"), o.get("product", "?"), o.get("amount", 0), + o.get("qty", 1), o.get("status", "?"), o.get("email", ""), + o.get("code", ""), o.get("date", ""), now)) + for u in data.get("users", []): + c.execute("""INSERT OR REPLACE INTO site_users + (site_id, user_id, username, email, membership, balance, orders, joined, ts) + VALUES (?,?,?,?,?,?,?,?,?)""", + (site_id, u.get("id"), u.get("username", "?"), u.get("email", ""), + u.get("membership", "free"), u.get("balance", 0), u.get("orders", 0), + u.get("joined", ""), now)) + for t in data.get("transactions", []): + c.execute("""INSERT OR REPLACE INTO site_txs + (site_id, tx_id, user, amount, method, type, date) + VALUES (?,?,?,?,?,?,?)""", + (site_id, t.get("id"), t.get("user", "?"), t.get("amount", 0), + t.get("method", ""), t.get("type", ""), t.get("date", ""))) + conn.commit() + conn.close() + orders = data.get("orders", []) + pending = sum(1 for o in orders if o.get("status") == "pending") + return { + "pending_orders": pending, + "delivered_orders": sum(1 for o in orders if o.get("status") == "delivered"), + "memberships": {u.get("membership", "free"): True for u in data.get("users", [])}, + "balance_total": round(sum(u.get("balance", 0) for u in data.get("users", [])), 2), + } + except Exception: + return None + +def poll_btcpay(): + """Poll BTCPay Server for sync status, store data, and invoice counts.""" + try: + # Server info (sync status) + r = requests.get(f"{BTCPAY_URL}/api/v1/server/info", + headers={"Authorization": f"token {BTCPAY_KEY}"}, + timeout=10, verify=False) + if r.status_code != 200: + return {"error": f"BTCPay returned {r.status_code}", "synced": False} + + info = r.json() + sync_pct = 0 + blocks = 0 + headers = 0 + for s in info.get("syncStatus", []): + ni = s.get("nodeInformation", {}) + if ni.get("headers") and ni.get("blocks"): + blocks = ni["blocks"] + headers = ni["headers"] + sync_pct = (blocks / headers) * 100 if headers else 0 + elif ni.get("verificationProgress"): + sync_pct = ni["verificationProgress"] * 100 + + # Per-store data + stores_data = {} + total_invoices = 0 + stores_with_wallet = 0 + + for sid, store_id in BTCPAY_STORES.items(): + try: + # Check payment methods + pm_r = requests.get(f"{BTCPAY_URL}/api/v1/stores/{store_id}/payment-methods", + headers={"Authorization": f"token {BTCPAY_KEY}"}, + timeout=5, verify=False) + has_wallet = False + if pm_r.status_code == 200: + pms = pm_r.json() + has_wallet = any(pm.get("enabled") for pm in pms if isinstance(pm, dict)) + + if has_wallet: + stores_with_wallet += 1 + + # Count invoices + inv_r = requests.get(f"{BTCPAY_URL}/api/v1/stores/{store_id}/invoices?take=100", + headers={"Authorization": f"token {BTCPAY_KEY}"}, + timeout=5, verify=False) + invoice_count = 0 + invoices_by_status = {} + if inv_r.status_code == 200: + invoices = inv_r.json() + invoice_count = len(invoices) + total_invoices += invoice_count + for inv in invoices: + status = inv.get("status", "unknown") + invoices_by_status[status] = invoices_by_status.get(status, 0) + 1 + + stores_data[sid] = { + "has_wallet": has_wallet, + "invoice_count": invoice_count, + "invoices_by_status": invoices_by_status, + "store_id": store_id[:16] + "...", + } + except Exception as e: + stores_data[sid] = {"has_wallet": False, "invoice_count": 0, "error": str(e)[:80]} + + # Save snapshot to DB + conn = sqlite3.connect(DB_PATH) + c = conn.cursor() + c.execute( + "INSERT INTO btcpay_snapshots (timestamp, sync_pct, blocks, headers, synced, store_count, stores_with_wallet, total_invoices) VALUES (?,?,?,?,?,?,?,?)", + (time.time(), round(sync_pct, 2), blocks, headers, + 1 if info.get("fullySynched") else 0, len(BTCPAY_STORES), stores_with_wallet, total_invoices) + ) + conn.commit() + conn.close() + + return { + "synced": info.get("fullySynched", False), + "sync_pct": round(sync_pct, 2), + "blocks": blocks, + "headers": headers, + "version": info.get("version", "?"), + "stores_with_wallet": stores_with_wallet, + "total_stores": len(BTCPAY_STORES), + "total_invoices": total_invoices, + "stores": stores_data, + "error": None, + } + except Exception as e: + return {"error": str(e)[:200], "synced": False, "sync_pct": 0, "stores": {}} + +def poll_all(): + """Poll all 10 sites + BTCPay and update state.""" + global state + results = {} + total_users = 0 + total_products = 0 + online = 0 + + for sid, info in SITES.items(): + result = poll_site(sid, info) + details = poll_site_details(sid, info) if result["status"] == "online" else None + if details: + result["details"] = details + results[sid] = result + total_users += result["users"] + total_products += result["products"] + if result["status"] == "online": + online += 1 + + # BTCPay poll + btcpay_data = poll_btcpay() + + # Store snapshots in DB + now = time.time() + conn = sqlite3.connect(DB_PATH) + c = conn.cursor() + for sid, r in results.items(): + c.execute( + "INSERT INTO snapshots (site_id, timestamp, users, products, orders, revenue, status, response_ms) VALUES (?,?,?,?,?,?,?,?)", + (sid, now, r["users"], r["products"], r.get("orders", 0), r.get("revenue", 0), r["status"], r.get("response_ms", 0)) + ) + conn.commit() + conn.close() + + with state_lock: + old_state = state.get("sites", {}) + for sid, r in results.items(): + old_users = old_state.get(sid, {}).get("users", 0) + old_orders = old_state.get(sid, {}).get("orders", 0) + old_revenue = old_state.get(sid, {}).get("revenue", 0) + + if r["users"] > old_users and old_users > 0: + event_detail = f"{r['emoji']} {r['name']}: +{r['users'] - old_users} new signup(s)" + c = sqlite3.connect(DB_PATH).cursor() + c.execute("INSERT INTO events (site_id, timestamp, event_type, detail) VALUES (?,?,?,?)", + (sid, now, "new_users", event_detail)) + c.connection.commit() + c.connection.close() + + if r["orders"] > old_orders and old_orders > 0: + new_orders = r["orders"] - old_orders + new_rev = r["revenue"] - old_revenue + event_detail = f"{r['emoji']} {r['name']}: +{new_orders} order(s) | +${new_rev:.2f} revenue" + c = sqlite3.connect(DB_PATH).cursor() + c.execute("INSERT INTO events (site_id, timestamp, event_type, detail) VALUES (?,?,?,?)", + (sid, now, "new_orders", event_detail)) + c.connection.commit() + c.connection.close() + + # BTCPay events + old_btcpay = old_state.get("btcpay", {}) if isinstance(old_state, dict) else {} + if btcpay_data.get("synced") and not old_btcpay.get("synced"): + event_detail = "โ‚ฟ BTCPay: Node fully synced! Wallets can be generated." + c = sqlite3.connect(DB_PATH).cursor() + c.execute("INSERT INTO events (site_id, timestamp, event_type, detail) VALUES (?,?,?,?)", + (0, now, "btcpay_synced", event_detail)) + c.connection.commit() + c.connection.close() + + total_revenue = sum(r.get("revenue", 0) for r in results.values()) + total_orders = sum(r.get("orders", 0) for r in results.values()) + + state = { + "sites": results, + "totals": { + "users": total_users, + "products": total_products, + "online": online, + "offline": 10 - online, + "orders": total_orders, + "revenue": round(total_revenue, 2), + }, + "last_poll": datetime.now().strftime("%H:%M:%S"), + "last_poll_ts": now, + "btcpay": btcpay_data, + } + +def poller_loop(): + """Background thread that polls every 30 seconds.""" + while True: + try: + poll_all() + except Exception as e: + print(f"Poll error: {e}") + time.sleep(30) + +@app.route('/') +def dashboard(): + return render_template('dashboard.html') + +@app.route('/master') +def master(): + return render_template('master.html') + +@app.route('/btcpay') +def btcpay_dash(): + """Dedicated BTCPay management view.""" + return render_template('btcpay.html') + +@app.route('/recon') +def recon(): + """Payment processor recon tool.""" + return app.send_static_file('payment-recon/index.html') + +@app.route('/watchdog') +def watchdog_page(): + """Self-healing watchdog status page.""" + return render_template('watchdog.html') + +@app.route('/api/watchdog') +def api_watchdog(): + if WATCHDOG_AVAILABLE: + return jsonify(get_watchdog_state()) + return jsonify({"error": "Watchdog not loaded"}) + +@app.route('/api/watchdog/run', methods=['POST']) +def api_watchdog_run(): + """Manually trigger a watchdog scan.""" + if WATCHDOG_AVAILABLE: + watchdog_scan() + return jsonify({"status": "ok", "result": get_watchdog_state()}) + return jsonify({"error": "Watchdog not available"}) + +@app.route('/api/state') +def api_state(): + with state_lock: + s = dict(state) + + # Get recent events + conn = sqlite3.connect(DB_PATH) + c = conn.cursor() + c.execute("SELECT site_id, timestamp, event_type, detail FROM events ORDER BY timestamp DESC LIMIT 50") + events = [{"site_id": r[0], "timestamp": r[1], "type": r[2], "detail": r[3]} for r in c.fetchall()] + conn.close() + + s["events"] = events + s["btc_address"] = BTC_ADDRESS + + # Load BTC stats from blockchain monitor DB + try: + btc_conn = sqlite3.connect("/Users/drjones/ccsite-manager/btc_chain.db") + bc = btc_conn.cursor() + bc.execute("SELECT COUNT(*), COALESCE(SUM(amount_btc),0), COALESCE(SUM(amount_sats),0) FROM transactions") + btc_count, btc_total, btc_sats = bc.fetchone() + bc.execute("SELECT COUNT(*), COALESCE(SUM(amount_btc),0) FROM transactions WHERE first_seen > ?", (time.time() - 86400,)) + btc_day_count, btc_day_btc = bc.fetchone() + bc.execute("SELECT txid, amount_btc, amount_sats, timestamp FROM transactions ORDER BY timestamp DESC LIMIT 5") + btc_recent = [{"txid": r[0][:16]+"...", "btc": r[1], "sats": r[2], "ts": r[3]} for r in bc.fetchall()] + btc_conn.close() + s["btc_stats"] = { + "total_txs": btc_count, "total_btc": round(btc_total or 0, 8), + "total_sats": btc_sats or 0, "total_usd": round((btc_total or 0) * 68000, 2), + "day_txs": btc_day_count, "day_btc": round(btc_day_btc or 0, 8), + "day_usd": round((btc_day_btc or 0) * 68000, 2), "recent": btc_recent, + } + except: + s["btc_stats"] = {"total_txs": 0, "total_btc": 0, "total_usd": 0, "day_btc": 0, "recent": []} + + # Revenue history for chart (last 24h: per-site latest value per hour, summed) + conn = sqlite3.connect(DB_PATH) + c = conn.cursor() + since = time.time() - 86400 + c.execute("""SELECT site_id, timestamp, revenue FROM snapshots + WHERE timestamp > ? ORDER BY timestamp ASC""", (since,)) + per_site_hour = {} + for sid, ts, rev in c.fetchall(): + per_site_hour[(sid, int(ts // 3600))] = rev # later rows overwrite -> latest wins + hourly = defaultdict(float) + for (sid, hr), rev in per_site_hour.items(): + hourly[hr] += rev + history = [{"ts": hr * 3600, "revenue": round(rev, 2)} for hr, rev in sorted(hourly.items())] + conn.close() + s["history"] = history + + # BTCPay history + conn = sqlite3.connect(DB_PATH) + c = conn.cursor() + c.execute("""SELECT timestamp, sync_pct FROM btcpay_snapshots + WHERE timestamp > ? ORDER BY timestamp ASC LIMIT 100""", (since,)) + btcpay_history = [{"ts": r[0], "sync_pct": r[1]} for r in c.fetchall()] + conn.close() + s["btcpay_history"] = btcpay_history + + return jsonify(s) + +@app.route('/api/history') +def api_history(): + """Get historical data for charts.""" + hours = int(request.args.get('hours', 24)) + since = time.time() - (hours * 3600) + + conn = sqlite3.connect(DB_PATH) + c = conn.cursor() + c.execute("""SELECT site_id, timestamp, users, status FROM snapshots + WHERE timestamp > ? ORDER BY timestamp ASC""", (since,)) + rows = c.fetchall() + conn.close() + + # Group by site + history = defaultdict(list) + for row in rows: + history[row[0]].append({ + "ts": row[1], + "users": row[2], + "status": row[3], + }) + + # Convert to serializable format + result = {} + for sid, points in history.items(): + # Downsample to ~100 points max + step = max(1, len(points) // 100) + result[str(sid)] = points[::step] + + return jsonify(result) + +@app.route('/api/orders') +def api_orders(): + """Cross-site live order feed, newest first.""" + limit = int(request.args.get('limit', 40)) + conn = sqlite3.connect(DB_PATH) + c = conn.cursor() + c.execute("""SELECT site_id, product, amount, qty, status, email, code, date + FROM site_orders ORDER BY date DESC LIMIT ?""", (limit,)) + rows = [{"site_id": r[0], "site": SITES.get(r[0], {}).get("name", "?"), + "emoji": SITES.get(r[0], {}).get("emoji", ""), + "product": r[1], "amount": r[2], "qty": r[3], "status": r[4], + "email": r[5], "code": r[6], "date": r[7]} for r in c.fetchall()] + conn.close() + return jsonify(rows) + +@app.route('/api/signups') +def api_signups(): + """Cross-site signup feed, newest first, plus membership breakdown.""" + limit = int(request.args.get('limit', 40)) + conn = sqlite3.connect(DB_PATH) + c = conn.cursor() + c.execute("""SELECT site_id, username, email, membership, balance, orders, joined + FROM site_users ORDER BY joined DESC LIMIT ?""", (limit,)) + rows = [{"site_id": r[0], "site": SITES.get(r[0], {}).get("name", "?"), + "emoji": SITES.get(r[0], {}).get("emoji", ""), + "username": r[1], "email": r[2], "membership": r[3], + "balance": r[4], "orders": r[5], "joined": r[6]} for r in c.fetchall()] + c.execute("SELECT membership, COUNT(*) FROM site_users GROUP BY membership") + tier_labels = {"0": "free", "1": "pro", "2": "elite", 0: "free", 1: "pro", 2: "elite"} + tiers = {tier_labels.get(r[0], r[0] or "free"): r[1] for r in c.fetchall()} + c.execute("SELECT site_id, COUNT(*) FROM site_users GROUP BY site_id") + per_site = {str(r[0]): r[1] for r in c.fetchall()} + conn.close() + return jsonify({"recent": rows, "tiers": tiers, "per_site": per_site}) + +@app.route('/api/leaderboard') +def api_leaderboard(): + """Per-site ranked performance table.""" + conn = sqlite3.connect(DB_PATH) + c = conn.cursor() + board = [] + for sid, info in SITES.items(): + c.execute("SELECT COUNT(*), COALESCE(SUM(amount*qty),0), COALESCE(AVG(amount*qty),0) FROM site_orders WHERE site_id=?", (sid,)) + n_orders, rev, aov = c.fetchone() + c.execute("SELECT COUNT(*) FROM site_orders WHERE site_id=? AND status='pending'", (sid,)) + pending = c.fetchone()[0] + c.execute("SELECT COUNT(*) FROM site_users WHERE site_id=?", (sid,)) + n_users = c.fetchone()[0] + c.execute("SELECT COUNT(*) FROM site_orders WHERE site_id=? AND date > datetime('now','-1 day')", (sid,)) + orders_24h = c.fetchone()[0] + board.append({"site_id": sid, "site": info["name"], "emoji": info["emoji"], + "orders": n_orders, "revenue": round(rev, 2), "aov": round(aov, 2), + "pending": pending, "users": n_users, "orders_24h": orders_24h}) + conn.close() + board.sort(key=lambda x: x["revenue"], reverse=True) + return jsonify(board) + +@app.route('/api/top-products') +def api_top_products(): + """Best-selling products across the fleet by order count and revenue.""" + conn = sqlite3.connect(DB_PATH) + c = conn.cursor() + c.execute("""SELECT product, COUNT(*), SUM(amount*qty), COUNT(DISTINCT site_id) + FROM site_orders GROUP BY product ORDER BY COUNT(*) DESC, SUM(amount*qty) DESC LIMIT 15""") + rows = [{"product": r[0], "orders": r[1], "revenue": round(r[2] or 0, 2), + "sites": r[3]} for r in c.fetchall()] + conn.close() + return jsonify(rows) + +@app.route('/api/activity') +def api_activity(): + """Hourly buckets for the last 48h: orders, revenue, signups. Plus 14-day daily.""" + conn = sqlite3.connect(DB_PATH) + c = conn.cursor() + hourly = {} + c.execute("""SELECT substr(date,1,13), COUNT(*), SUM(amount*qty) FROM site_orders + WHERE date > datetime('now','-2 day') GROUP BY substr(date,1,13)""") + for r in c.fetchall(): + hourly[r[0]] = {"orders": r[1], "revenue": round(r[2] or 0, 2)} + c.execute("""SELECT substr(joined,1,13), COUNT(*) FROM site_users + WHERE joined > datetime('now','-2 day') GROUP BY substr(joined,1,13)""") + for r in c.fetchall(): + hourly.setdefault(r[0], {})["signups"] = r[1] + daily = {} + c.execute("""SELECT substr(date,1,10), COUNT(*), SUM(amount*qty) FROM site_orders + WHERE date > datetime('now','-14 day') GROUP BY substr(date,1,10)""") + for r in c.fetchall(): + daily[r[0]] = {"orders": r[1], "revenue": round(r[2] or 0, 2)} + c.execute("""SELECT substr(joined,1,10), COUNT(*) FROM site_users + WHERE joined > datetime('now','-14 day') GROUP BY substr(joined,1,10)""") + for r in c.fetchall(): + daily.setdefault(r[0], {})["signups"] = r[1] + conn.close() + return jsonify({"hourly": hourly, "daily": daily}) + +@app.route('/api/funnel') +def api_funnel(): + """Fleet conversion funnel + headline numbers.""" + conn = sqlite3.connect(DB_PATH) + c = conn.cursor() + c.execute("SELECT COUNT(*) FROM site_users") + users = c.fetchone()[0] + c.execute("SELECT COUNT(*) FROM site_users WHERE orders > 0") + buyers = c.fetchone()[0] + c.execute("SELECT COUNT(*), COALESCE(SUM(amount*qty),0) FROM site_orders") + orders, revenue = c.fetchone() + c.execute("SELECT COUNT(*) FROM site_orders WHERE status='pending'") + pending = c.fetchone()[0] + c.execute("SELECT COUNT(*) FROM site_orders WHERE status='delivered'") + delivered = c.fetchone()[0] + c.execute("SELECT COALESCE(SUM(amount),0) FROM site_txs WHERE type='deposit'") + deposits = c.fetchone()[0] + c.execute("SELECT COUNT(DISTINCT email) FROM site_orders") + unique_customers = c.fetchone()[0] + conn.close() + return jsonify({ + "users": users, "buyers": buyers, "orders": orders, + "revenue": round(revenue, 2), "pending": pending, "delivered": delivered, + "deposits": round(deposits, 2), "unique_customers": unique_customers, + "conversion": round(100.0 * buyers / users, 1) if users else 0, + "aov": round(revenue / orders, 2) if orders else 0, + }) + +if __name__ == '__main__': + init_db() + poll_all() + t = threading.Thread(target=poller_loop, daemon=True) + t.start() + # Start watchdog + if WATCHDOG_AVAILABLE: + import watchdog + wt = threading.Thread(target=watchdog.watchdog_loop, daemon=True) + wt.start() + watchdog.run_all_checks() + print(f" /watchdog โ€” Self-healing health monitor") + print(f"Dashboard starting on http://localhost:5050") + print(f" / โ€” Fleet dashboard") + print(f" /master โ€” Master control panel") + print(f" /btcpay โ€” BTCPay management") + app.run(host='0.0.0.0', port=5050, debug=False) diff --git a/payment-recon b/payment-recon new file mode 160000 index 0000000..5ed685a --- /dev/null +++ b/payment-recon @@ -0,0 +1 @@ +Subproject commit 5ed685a79ab3b67eaa8e4d717e99d0088dbd88a8 diff --git a/payment-recon.html b/payment-recon.html new file mode 100644 index 0000000..0f706cc --- /dev/null +++ b/payment-recon.html @@ -0,0 +1,397 @@ + + + + + +Payment Recon โ€” Site Explorer + + + + +
+
+

๐Ÿ” Payment Recon

+
+
+
+ + + + +
+
+ +
+ + + + +
+ +
+ +
+
๐Ÿ”

No sites cataloged yet.

Click "+ Add Site" to start building your database,
or use the Dorks panel to discover real sites on Google.

+
+
+ + + + + + + + + + + + + diff --git a/templates/btcpay.html b/templates/btcpay.html new file mode 100644 index 0000000..b9678d1 --- /dev/null +++ b/templates/btcpay.html @@ -0,0 +1,229 @@ + + + + + + BTCPay Manager โ€” CCSite Fleet + + + + +
+
+

โ‚ฟ BTCPay Server Manager

+ 10.30.20.140 ยท -- +
+ +
+ +
+ +
+
+
+
Sync Progress
+
--
+
+
+
+
+ -- + -- +
+
+ Syncing + v-- +
+
+
+
Wallets Ready
+
0/10
+
Total Invoices: 0
+
+
+
+ + +
+ + +
+
+

๐Ÿ“ˆ Sync Progress (24h)

+ +
+
+

๐Ÿช Per-Store Invoice Count

+ +
+
+ + +
+

๐Ÿ“‹ Store Status

+ + + +
#SiteStore IDWalletInvoicesWebhookStatus
+
+ + +
+

๐Ÿ”— Webhook Status

+
Loading...
+
+
+ + + + diff --git a/templates/dashboard.html b/templates/dashboard.html new file mode 100644 index 0000000..6f01fa3 --- /dev/null +++ b/templates/dashboard.html @@ -0,0 +1,324 @@ + + + + + +FLEET OPS โ€” CCSite Mission Control + + + + +
+
+ +
+

FLEET OPS

+
CCSite Mission Control
+
+ +
+
+ live ยท poll โ€” + โ€” +
+
+ +
+ +
+

Revenue โ€” fleet history

+

Activity โ€” 14 days orders vs signups

+

Users by site

+

BTC node

+
+ +
+
+

Fleet โ€” 10 storefronts

+
+
+

Conversion funnel

+
+
+

๐Ÿ’ธ Live orders

+

๐Ÿ‘ค Signups

+

โšก Events

+
+
+ +
+

๐Ÿ† Site leaderboard โ€” by revenue

+

๐Ÿ”ฅ Top products

+

โ‚ฟ BTCPay stores

+
+

Recent chain activity

+
+
+
+ + + + diff --git a/templates/master.html b/templates/master.html new file mode 100644 index 0000000..9499a29 --- /dev/null +++ b/templates/master.html @@ -0,0 +1,257 @@ + + + + + Master Control โ€” CCSite Fleet + + + + +
+

๐Ÿ–ฅ๏ธ Master Control Panel

+ +
+
+
๐Ÿ“Š Overview
+
๐Ÿ“ฆ Orders
+
๐Ÿ‘ฅ Users
+
๐Ÿท๏ธ Products
+
๐Ÿ’ณ Transactions
+
+
+
+
+
+
+

๐Ÿ“ฆ Recent Orders

+
+
+
+

๐Ÿ‘ฅ Recent Signups

+
+
+
+
+
+ +
SiteOrderProductEmailAmountStatusDate
+
+
+ +
SiteUserEmailBalanceOrdersJoined
+
+
+ +
SiteProductCategoryPriceStock
+
+
+ +
SiteUserTypeMethodAmountDate
+
+
+ + + + diff --git a/templates/watchdog.html b/templates/watchdog.html new file mode 100644 index 0000000..46f49ee --- /dev/null +++ b/templates/watchdog.html @@ -0,0 +1,116 @@ + + + + + +Watchdog โ€” CCSite Fleet + + + +

๐Ÿ›ก๏ธ CCSite Watchdog

+

Self-healing health monitor โ€” checks every 60s

+ +
+
+
+ +
+ + +
+ +
+

+ + + + diff --git a/watchdog.py b/watchdog.py new file mode 100644 index 0000000..43f0c1b --- /dev/null +++ b/watchdog.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +""" +CCSite Watchdog โ€” Self-healing health monitor. +Extends the dashboard with automated checks, self-healing, and alerts. +""" +import json, time, sqlite3, subprocess, threading, sys +from datetime import datetime +import requests, urllib3 +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +# โ”€โ”€โ”€ Config โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +SITES = { + 1: {"name": "VaultPass", "ip": "10.30.20.210", "emoji": "๐ŸŽฎ", "vmid": 123}, + 2: {"name": "StreamPass", "ip": "10.30.20.211", "emoji": "๐ŸŽฌ", "vmid": 124}, + 3: {"name": "KeyForge", "ip": "10.30.20.212", "emoji": "๐Ÿ’ป", "vmid": 125}, + 4: {"name": "SocialForge", "ip": "10.30.20.213", "emoji": "๐Ÿฆ", "vmid": 126}, + 5: {"name": "CardVault", "ip": "10.30.20.214", "emoji": "๐Ÿ’ณ", "vmid": 127}, + 6: {"name": "CloudNexus", "ip": "10.30.20.217", "emoji": "โ˜๏ธ", "vmid": 128}, + 7: {"name": "LearnForge", "ip": "10.30.20.218", "emoji": "๐Ÿ“š", "vmid": 129}, + 8: {"name": "PrivacyPass", "ip": "10.30.20.219", "emoji": "๐Ÿ›ก๏ธ", "vmid": 130}, + 9: {"name": "CreatorForge", "ip": "10.30.20.220", "emoji": "๐ŸŽจ", "vmid": 131}, + 10: {"name": "FoodVault", "ip": "10.30.20.221", "emoji": "๐Ÿ•", "vmid": 132}, +} +BTCPAY_URL = "https://10.30.20.140" +BTCPAY_KEY = "6026288e2e315984661c748baafd509e81a75f22" +BTCPAY_STORES = { + 1: "AYZWEAt63TgY4QaxwNBawHASMKcimJabcZxSczRqZG81", + 2: "5pi94zmY3F26pTZJ99JfsFG9pb8atbz7fpALrPjS2BRq", + 3: "3mc58myPrkdN7hRFdsBwJxbVgBQQ3VvSYZygKQC3FiSG", + 4: "E1uZ5nmU6qSpMB8sna7fNdxZFiem9JmkxB9LdJkFN23B", + 5: "BcvaUvv8MM8if5dzqe5yLnNERG6EwjjmeMok54ATqjWf", + 6: "GvSsA9xaqymhswsiPo4APHMuT2sXCCtnBLgPWcHigxVx", + 7: "A2TXbpkpj88Wtj34dLS9yzjMgmcodysEaRhTjc3rFcUr", + 8: "9N8dTC99uXn3W3e2F9o8kVQ5ewijh87NevKmMATF1Jcd", + 9: "39BKRF3zQb6Gi8LiKo1US13bzJfhpCzkwoHP6pCyQFwD", + 10: "9vCjKrKbwgey4GH7DBAMa6FgkiApgHvd5khFpWSZzJL2", +} +SSH = "sshpass -p 'Czapiewski1!' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 root@10.30.20.85" + +watchdog_state = {"checks": [], "issues": [], "healing_actions": [], "last_check": None} +state_lock = threading.Lock() + +# โ”€โ”€โ”€ Checks โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +def check_site_http(site_id, info): + """Check if a site returns HTTP 200.""" + try: + r = requests.get(f"http://{info['ip']}:5000/", timeout=5) + return {"check": f"{info['emoji']} {info['name']} HTTP", "status": "pass" if r.status_code == 200 else "warn", "detail": f"HTTP {r.status_code}"} + except: + return {"check": f"{info['emoji']} {info['name']} HTTP", "status": "fail", "detail": "unreachable"} + +def check_site_btcpay_config(site_id, info): + """Check if the site is reachable (BTCPay config verified separately).""" + try: + r = requests.get(f"http://{info['ip']}:5000/", timeout=5) + return {"check": f"{info['emoji']} {info['name']} Site", "status": "pass" if r.status_code == 200 else "warn", "detail": f"HTTP {r.status_code}"} + except: + return {"check": f"{info['emoji']} {info['name']} BTCPay config", "status": "fail", "detail": "unreachable"} + +def check_btcpay_sync(): + """Check BTCPay sync status.""" + try: + r = requests.get(f"{BTCPAY_URL}/api/v1/server/info", headers={"Authorization": f"token {BTCPAY_KEY}"}, timeout=10, verify=False) + if r.status_code == 200: + info = r.json() + synced = info.get("fullySynched", False) + for s in info.get("syncStatus", []): + ni = s.get("nodeInformation", {}) + if ni.get("headers"): + pct = (ni["blocks"] / ni["headers"] * 100) if ni["headers"] else 0 + return {"check": "โ‚ฟ BTCPay Sync", "status": "pass" if synced else "warn", "detail": f"{pct:.1f}% ({ni['blocks']}/{ni['headers']})"} + return {"check": "โ‚ฟ BTCPay Sync", "status": "pass" if synced else "warn", "detail": f"synced={synced}"} + return {"check": "โ‚ฟ BTCPay Sync", "status": "fail", "detail": f"HTTP {r.status_code}"} + except Exception as e: + return {"check": "โ‚ฟ BTCPay Sync", "status": "fail", "detail": str(e)[:80]} + +def check_wallet_health(): + """Check how many stores have working wallets by creating test invoices.""" + healthy = 0 + for sid, store_id in BTCPAY_STORES.items(): + try: + # Try creating a 0.01 invoice โ€” if wallet works, it'll succeed + r = requests.post(f"{BTCPAY_URL}/api/v1/stores/{store_id}/invoices", + headers={"Authorization": f"token {BTCPAY_KEY}", "Content-Type": "application/json"}, + json={"amount": "5.00", "currency": "USD"}, timeout=8, verify=False) + if r.status_code == 200: + healthy += 1 + except: + pass + total = len(BTCPAY_STORES) + return {"check": "โ‚ฟ Wallet Health", "status": "pass" if healthy == total else "warn" if healthy > 0 else "fail", + "detail": f"{healthy}/{total} stores can create invoices"} + +def check_webhook_health(): + """Check webhooks are enabled on all stores.""" + ok = 0 + total = len(BTCPAY_STORES) + for sid, store_id in BTCPAY_STORES.items(): + try: + r = requests.get(f"{BTCPAY_URL}/api/v1/stores/{store_id}/webhooks", headers={"Authorization": f"token {BTCPAY_KEY}"}, timeout=5, verify=False) + if r.status_code == 200: + hooks = r.json() + if hooks and hooks[0].get("enabled"): + ok += 1 + except: + pass + return {"check": "๐Ÿ”— Webhooks", "status": "pass" if ok == total else "warn" if ok > 0 else "fail", "detail": f"{ok}/{total} enabled"} + +# โ”€โ”€โ”€ Self-Healing โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +def heal_restart_site(site_id, info): + """Restart a site's shop service.""" + try: + result = subprocess.run(f"{SSH} 'pct exec {info['vmid']} -- systemctl restart shop'", + shell=True, capture_output=True, text=True, timeout=15) + success = result.returncode == 0 + return {"action": f"Restart {info['name']}", "success": success, "detail": "restarted" if success else result.stderr[:80]} + except Exception as e: + return {"action": f"Restart {info['name']}", "success": False, "detail": str(e)[:80]} + +def heal_generate_wallet(store_id, site_name): + """Try to generate a wallet for a store.""" + try: + r = requests.post(f"{BTCPAY_URL}/api/v1/stores/{store_id}/payment-methods/BTC-CHAIN/wallet/generate", + headers={"Authorization": f"token {BTCPAY_KEY}", "Content-Type": "application/json"}, + json={"savePrivateKeys": True}, timeout=15, verify=False) + if r.status_code == 200: + return {"action": f"Generate wallet for {site_name}", "success": True, "detail": "generated"} + return {"action": f"Generate wallet for {site_name}", "success": False, "detail": f"HTTP {r.status_code}: {r.text[:80]}"} + except Exception as e: + return {"action": f"Generate wallet for {site_name}", "success": False, "detail": str(e)[:80]} + +# โ”€โ”€โ”€ Main Check Loop โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +def run_all_checks(): + """Run all watchdog checks and trigger healing if needed.""" + checks = [] + issues = [] + healing = [] + + # Site HTTP checks (one per site) + for sid, info in SITES.items(): + http_check = check_site_http(sid, info) + checks.append(http_check) + if http_check["status"] == "fail": + issues.append(f"{info['emoji']} {info['name']} is DOWN") + result = heal_restart_site(sid, info) + healing.append(result) + + # BTCPay health + sync_check = check_btcpay_sync() + checks.append(sync_check) + if sync_check["status"] == "fail": + issues.append("BTCPay is DOWN") + + wallet_check = check_wallet_health() + checks.append(wallet_check) + if wallet_check["status"] == "warn": + # Try healing missing wallets + for sid, store_id in BTCPAY_STORES.items(): + try: + r = requests.get(f"{BTCPAY_URL}/api/v1/stores/{store_id}/payment-methods/BTC-CHAIN", headers={"Authorization": f"token {BTCPAY_KEY}"}, timeout=5, verify=False) + if r.status_code == 200: + pm = r.json() + if not pm.get("walletId") and pm.get("derivationScheme") in (None, "", "NONE"): + result = heal_generate_wallet(store_id, SITES[sid]["name"]) + healing.append(result) + except: + pass + + webhook_check = check_webhook_health() + checks.append(webhook_check) + + # Summary + pass_count = sum(1 for c in checks if c["status"] == "pass") + warn_count = sum(1 for c in checks if c["status"] == "warn") + fail_count = sum(1 for c in checks if c["status"] == "fail") + + with state_lock: + watchdog_state["checks"] = checks + watchdog_state["issues"] = issues + watchdog_state["healing_actions"] = healing + watchdog_state["last_check"] = datetime.now().strftime("%H:%M:%S") + watchdog_state["summary"] = { + "total": len(checks), "pass": pass_count, "warn": warn_count, "fail": fail_count, + "status": "healthy" if fail_count == 0 and warn_count == 0 else "degraded" if fail_count == 0 else "critical" + } + + return watchdog_state + +def watchdog_loop(): + """Background thread โ€” runs checks every 60 seconds.""" + while True: + try: + run_all_checks() + except Exception as e: + print(f"Watchdog error: {e}", file=sys.stderr) + time.sleep(60) + +# โ”€โ”€โ”€ Flask Integration โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +def get_state(): + with state_lock: + return dict(watchdog_state) + +if __name__ == '__main__': + print("Watchdog starting...") + run_all_checks() + print(f" Checks: {watchdog_state['summary']}") + t = threading.Thread(target=watchdog_loop, daemon=True) + t.start() + # Keep alive + while True: + time.sleep(60)