#!/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)