commit 8e95d8a78c01812dc391e3bbe4c254e570d8e651 Author: drjones Date: Tue Oct 6 23:43:32 2026 -0700 Snapshot: full project state diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..24b2937 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +__pycache__/ +*.pyc +node_modules/ +.venv/ +venv/ +.env +*.db +*.sqlite* +*.log +.DS_Store +out/ +work/ +.pio/ +briefs/ +dns-backup/ +archive/ +*.png diff --git a/README.md b/README.md new file mode 100644 index 0000000..f21ded0 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# Landing Factory — Fleet Landing Page Generator + +Emits self-contained interactive landing pages (inline CSS+JS, zero external +deps) for homelab fleet sites. `python3 build.py` → `out/.html`. + +Also packs the deploy tooling: `deploy_landings.py` (push to CTs), +`full_home_tunnel.py` / `new_fleet_tunnel.py` (Cloudflare tunnel wiring), +`liveness.py` (post-deploy checks), plus the one-off patch scripts used to +fix the shim/satsspin generators. + diff --git a/build.py b/build.py new file mode 100644 index 0000000..c5f5d71 --- /dev/null +++ b/build.py @@ -0,0 +1,314 @@ +#!/usr/bin/env python3 +"""Landing Factory — kick-ass interactive landing pages for the homelab fleet. +Emits self-contained HTML (inline CSS+JS, zero external deps) per site. +Usage: python3 build.py -> writes ~/landing-factory/out/.html +""" +import os + +OUT = os.path.expanduser("~/landing-factory/out") +os.makedirs(OUT, exist_ok=True) + +ENGINE_CSS = """ + +""" + +ENGINE_JS = """ + +""" + +PAGES = { +"satsspin": dict( + title="SatsSpin — Lightning Casino", + ac="#ffb300", ac2="#7c3aed", bg="#0a0618", bg2="#150b2e", card="#141026", txt="#f2edff", mut="#9a8fc4", + glow="rgba(255,179,0,.45)", glowsoft="rgba(255,179,0,.15)", border="rgba(255,179,0,.5)", + band="rgba(124,58,237,.25)", + typephrases=['⚡ Instant Lightning payouts', '🎰 6 provably-fair games', '🔓 No KYC. No waiting.', '💜 Built for sats, not suits'], + slots=True, wave=False, + nav=[('Games','/play'),('How It Works','#how'),('Lightning','#lightning')], + navcta=('Play Now','/play'), + body="""
+
▌
+
LIVE · Lightning Network Casino
+

Spin The Lightning.

+

SatsSpin is a Bitcoin-native casino running on the Lightning Network. Deposit in seconds, win in sats, withdraw instantly. No accounts with strangers, no KYC, no payout delays — just you and the reels.

+ +
+
0
Casino Games
+
0
% Lightning Payouts
+
0
KYC Requirements
+
0
/ 7 Live
+
+
🎰SLOTS💣MINES🎲DICE📈CRASH🃏BLACKJACK🎯PLINKO⚡LIGHTNING
+
+
+

Pick Your Poison

+

Six games. One engine. Zero delays. Launch the casino →

+
+
🎰

Slots

Classic 3-reel action with 777 jackpots and multiplier lines. Pure dopamine, one spin away.

+
💣

Mines

Reveal safe tiles, dodge the bombs, cash out when your nerve breaks. Risk/reward at its finest.

+
🎲

Dice

Pick your odds, roll under the number, double or nothing. The simplest game in the house.

+
📈

Crash

The multiplier climbs until it doesn't. Cash out before the crash — greed is a strategy, until it isn't.

+
🃏

Blackjack

Beat the dealer to 21. Hit, stand, and count your sats when the house busts.

+
🎯

Plinko

Drop the ball, watch it bounce, pray to the peg gods. Big drops mean big multipliers.

+
+
+
+

Three Steps To Winning

+

Faster than a bank transfer to the moon.

+
+
01

Create a House Account

No KYC, no email hoops. Your account lives in the casino — anonymous by design.

+
02

⚡ Top Up With Lightning

Scan the invoice with any Lightning wallet. Sats land in seconds, not days.

+
03

Play & Withdraw Instantly

Bet sats, stack sats, withdraw sats. Payouts hit your wallet the moment you cash out.

+
+
+
+
+

⚡ Powered By The Lightning Network

+

Real Bitcoin, real fast. Deposits and withdrawals over Lightning — the same network the whole sats economy runs on.

+ ⚡ Enter The Casino +
+
+""" +), +"signalminer": dict( + title="Signal Miner X — Market Intelligence", + ac="#00e676", ac2="#00e5ff", bg="#03050a", bg2="#071018", card="#0d1420", txt="#e4e8f4", mut="#7a8699", + glow="rgba(0,230,118,.4)", glowsoft="rgba(0,230,118,.14)", border="rgba(0,230,118,.5)", + band="rgba(0,229,255,.16)", + typephrases=['📡 1,085+ Telegram channels monitored', '🎁 Gift cards before your competition', '🔑 Software keys, wholesale prices', '🤖 API access for your bots'], + slots=False, wave=True, + nav=[('Features','#features'),('Coverage','#coverage'),('Pricing','#pricing')], + navcta=('Launch App','/signup'), + body="""
+
▌
+
LIVE · 1,085+ Telegram Marketplaces
+

Mine The Signal.
Trade The Noise.

+

Signal Miner X watches the Telegram marketplace underground in real time. Deals on gift cards, software keys, streaming accounts and hardware — surfaced, credibility-scored, and searchable before your competition even opens the app.

+ + +
+
0
Channels Monitored
+
0
Commerce Categories
+
0
Gift Card Listings
+
0
KYC Checks
+
+
+
+

Built For Deal Hunters

+

Everything you need to find the signal in the noise.

+
+
🔍

Instant Search

Search every monitored channel for the exact deal you want. Results in seconds, not scrobbles.

+
🎯

Credibility Scoring

Every seller and channel gets a trust score built from history, behavior and network signals.

+
🤖

API Access

Wire Signal Miner into your own bots and alerting. Your API key, your pipeline.

+
🛒

776 Categories

Gift cards, software keys, accounts, hardware, services. If it's traded, it's tracked.

+
⚡

Real-Time Feeds

New listings land the moment they're posted. Speed is the entire game.

+
🔓

No KYC, Ever

Just an email for your key. Pay in Bitcoin. No forms, no ID, no nonsense.

+
+
+
+

The Underground, Mapped

+

1,085+ channels across 776 commerce categories — and counting.

+
+
🎁
0

Gift Cards

+
🔑
0

Software Keys

+
📺
0

Accounts

+
💻
0

Hardware

+
🛠️
0

Services & Tools

+
+
+
+

Ridiculously Simple Pricing

+

No subscriptions. Pay with Bitcoin. Get intelligence.

+
+

🆓 Free Tier

$0
forever
+
  • Free starter searches
  • Full channel access
  • Credibility scoring
  • No expiration
+ Start Free
+
MOST POPULAR

🚀 Researcher Tier

$1
= 5 searches · Bitcoin ⚡
+
  • 5 marketplace searches
  • Full access to 1,085+ channels
  • Credibility scoring
  • API access for bots
  • No expiration · No KYC
+ Sign Up Free
+
+
+
+
+

The Next Deal Is Already Posted.

+

Get your free searches and see what the underground is selling today.

+ Claim Free Searches +
+
+""" +), +"workbrain": dict( + title="Certus WorkBrain", + ac="#ffb300", ac2="#ff7043", bg="#0c0f14", bg2="#12161d", card="#161b23", txt="#eef1f5", mut="#8a94a3", + glow="rgba(255,179,0,.4)", glowsoft="rgba(255,179,0,.13)", border="rgba(255,179,0,.5)", + band="rgba(255,112,67,.14)", + typephrases=['🐜 Pest knowledge, indexed', '🧠 AI answers with citations', '📄 Every doc, instantly searchable', '🐛 Bug squashing on standby'], + slots=False, wave=False, + nav=[('Features','#features'),('Knowledge','#knowledge')], + navcta=('Open WorkBrain','/app'), + body="""
+
▌
+
CERTUS · KNOWLEDGE ENGINE
+

Your Second Brain
For Pest Control.

+

WorkBrain ingests your documents, chunks them into knowledge, and answers questions with citations. Chat, search, and squash bugs — one brain for the whole operation.

+ +
+
0
/ 7 Availability
+
0
% Cited Answers
+
0
Knowledge Pillars
+
0
Bug Squasher
+
+
+
+

Everything In One Place

+

Stop digging through folders. Ask the brain.

+
+
💬

AI Chat

Ask anything about your operations. Answers come with citations straight from your documents.

+
📄

Document Vault

Every ingested file, organized and retrievable. Drop it in, never lose it again.

+
🧩

Knowledge Chunks

Documents broken into searchable knowledge chunks — the raw material for every answer.

+
🐛

Bug Squasher

Click the bugs before they escape. The only pest-control game built into a knowledge base.

+
+
+
+
+

Stop Searching. Start Knowing.

+

Your entire pest-control knowledge base, one click away.

+ 🐜 Enter WorkBrain +
+
+""" +), +} + +def build(name): + p = PAGES[name] + css = (ENGINE_CSS + .replace("AC;--accent2:AC2", f"{p['ac']};--accent2:{p['ac2']}").replace("BG;--bg2:BG2", f"{p['bg']};--bg2:{p['bg2']}") + .replace("CARD;--text:TXT", f"{p['card']};--text:{p['txt']}").replace("--muted:MUT", f"--muted:{p['mut']}") + .replace("COLORGLOW_SOFT", p['glowsoft']).replace("COLORGLOW_BAND", p['band']) + .replace("COLORGLOW", p['glow']).replace("ACBORDER", p['border'])) + js = (ENGINE_JS + .replace('accent:"AC"', f'accent:"{p["ac"]}"').replace('accent2:"AC2"', f'accent2:"{p["ac2"]}"') + .replace('bg2:"BG2"', f'bg2:"{p["bg2"]}"').replace('glow:"COLORGLOW"', f'glow:"{p["glow"]}"') + .replace("wave:WAVE", f"wave:{str(p['wave']).lower()}").replace("slots:SLOTS", f"slots:{str(p['slots']).lower()}") + .replace("typephrases:TYPEPHRASES", "typephrases:" + repr(p['typephrases']))) + nav_html = "".join(f'{l}' for l, u in p["nav"]) + footer = (f'') + html = f""" + +{p['title']} +{css} + + +{p['body']} + +{footer} +{js}""" + path = os.path.join(OUT, f"{name}.html") + with open(path, "w") as f: + f.write(html) + print(f"built {path} ({len(html)} bytes)") + +for name in PAGES: + build(name) +print("DONE") diff --git a/deploy_landings.py b/deploy_landings.py new file mode 100644 index 0000000..0c70fd1 --- /dev/null +++ b/deploy_landings.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +"""Deploy landing pages to SatsSpin (CT146), Signal Miner (CT548), WorkBrain (CT144). +Run ON Proxmox: /usr/bin/python3 /tmp/deploy_landings.py +""" +import re, subprocess, sys + +def sh(cmd, timeout=60): + r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout) + return r.stdout + r.stderr + +def pct(vmid, cmd, timeout=60): + return sh(f"pct exec {vmid} -- bash -c {sh_quote(cmd)}", timeout) + +def sh_quote(s): + return "'" + s.replace("'", "'\\''") + "'" + +results = {} + +# ── 1. SatsSpin CT146: replace dist/index.html ────────────────────── +print("=== SATSSPIN CT146 ===") +out = pct(146, "cp /opt/casino/dist/index.html /opt/casino/dist/index.html.aistudio.bak && " + "cp /tmp/satsspin.html /opt/casino/dist/index.html && " + "chmod 644 /opt/casino/dist/index.html && echo COPIED") +results['satsspin'] = 'COPIED' in out +print(out[:200]) +sh("pct exec 146 -- systemctl restart satsspin", timeout=60) +print("restarted satsspin") + +# ── 2. Signal Miner CT548: landing file + patch "/" route ─────────── +print("=== SIGNAL MINER CT548 ===") +out = pct(548, "cp /opt/signal-miner/app.py /opt/signal-miner/app.py.bak-landing-20260813 && " + "cp /tmp/signalminer.html /opt/signal-miner/landing.html && echo BACKED_UP") +results['signalminer_backup'] = 'BACKED_UP' in out +print(out[:200]) + +# patch app.py: replace old landing() body with file read +patch_script = ''' +import re +src = open("/opt/signal-miner/app.py").read() +old_start = src.find("@app.route(\\"/\\")\\ndef landing():") +assert old_start != -1, "landing route not found" +old_end = src.find("\\"\\"\\"", old_start) +assert old_end != -1, "landing end marker not found" +old_end += len("\\"\\"\\"") +new_code = """@app.route(\\"/\\") +def landing(): + with open("/opt/signal-miner/landing.html") as f: + return f.read()""" +src = src[:old_start] + new_code + src[old_end:] +open("/opt/signal-miner/app.py","w").write(src) +print("PATCHED") +''' +open("/tmp/patch_sm.py", "w").write(patch_script) +sh("pct push 548 /tmp/patch_sm.py /tmp/patch_sm.py", timeout=60) +out = pct(548, "python3 /tmp/patch_sm.py && systemctl restart signal-miner && echo RESTARTED") +results['signalminer'] = 'RESTARTED' in out +print(out[:300]) + +# ── 3. WorkBrain CT144: landing file + "/" → landing, "/app" → dashboard ── +print("=== WORKBRAIN CT144 ===") +svc = pct(144, "systemctl list-units --type=service --state=running | grep -oiE '[a-z-]*workbrain[a-z-]*' | head -1").strip() +print("service:", svc or "?") +out = pct(144, "cp /opt/workbrain/app.py /opt/workbrain/app.py.bak-landing-20260813 && " + "cp /tmp/workbrain.html /opt/workbrain/landing.html && echo BACKED_UP") +results['workbrain_backup'] = 'BACKED_UP' in out +print(out[:200]) + +patch_script2 = ''' +src = open("/opt/workbrain/app.py").read() +old = "@app.route(\\"/\\")\\ndef dashboard():\\n return render_template(\\"dashboard.html\\")" +new = """@app.route(\\"/\\") +def landing(): + with open("/opt/workbrain/landing.html") as f: + return f.read() + +@app.route(\\"/app\\") +def dashboard(): + return render_template(\\"dashboard.html\\")""" +assert old in src, "dashboard route pattern not found" +src = src.replace(old, new, 1) +open("/opt/workbrain/app.py","w").write(src) +print("PATCHED") +''' +open("/tmp/patch_wb.py", "w").write(patch_script2) +sh("pct push 144 /tmp/patch_wb.py /tmp/patch_wb.py", timeout=60) +out = pct(144, "python3 /tmp/patch_wb.py && echo PATCHED") +print(out[:200]) +if svc: + out = pct(144, f"systemctl restart {svc} && echo RESTARTED") + results['workbrain'] = 'RESTARTED' in out + print(out[:200]) +else: + out = pct(144, "systemctl list-units --type=service --state=running | grep -iE 'flask|gunicorn|python' | head -3") + print("fallback services:", out[:300]) + results['workbrain'] = 'manual' + +print("=== RESULTS ===") +for k, v in results.items(): + print(k, "OK" if v else "FAILED") diff --git a/fix_nan.py b/fix_nan.py new file mode 100644 index 0000000..d8a4fac --- /dev/null +++ b/fix_nan.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +"""SatsSpin: default logged-out balance displays to 0 instead of NaN.""" +import io + +B = "/opt/casino/dist/assets/index-CzzrlZy-.js" +bundle = io.open(B, encoding="utf-8").read() + +fixes = [ + ("Te(h.balanceSats)", "Te(h.balanceSats||0)"), + ("sb(h.balanceSats)", "sb(h.balanceSats||0)"), + ("Lt(h.balanceSats)", "Lt(h.balanceSats||0)"), +] +for old, new in fixes: + n = bundle.count(old) + bundle = bundle.replace(old, new) + print(f"{old} -> {new}: {n} replaced") + +io.open(B, "w", encoding="utf-8").write(bundle) +print("NAN FIX DONE") diff --git a/fix_shim_var.py b/fix_shim_var.py new file mode 100644 index 0000000..e1eda14 --- /dev/null +++ b/fix_shim_var.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +"""Fix SatsSpin shim: second fetch wrapper must use its own captured ref.""" +import io + +P = "/opt/casino/dist/shim.js" +src = io.open(P, encoding="utf-8").read() + +old1 = " // Store token when create/login response hits\n _fetch = window.fetch;" +new1 = " // Store token when create/login response hits\n var _fetch2 = window.fetch;" +assert old1 in src, "second wrapper assignment not found" +src = src.replace(old1, new1, 1) + +old2 = "return _fetch.call(this, url, opts)" +assert src.count(old2) == 1, f"expected 1 _fetch.call, found {src.count(old2)}" +src = src.replace(old2, "return _fetch2.call(this, url, opts)", 1) + +io.open(P, "w", encoding="utf-8").write(src) + +A = "/opt/casino/dist/app.html" +app = io.open(A, encoding="utf-8").read() +app = app.replace('shim.js?v=2', 'shim.js?v=3') +io.open(A, "w", encoding="utf-8").write(app) +print("SECOND WRAPPER FIXED + v=3") diff --git a/full_home_tunnel.py b/full_home_tunnel.py new file mode 100644 index 0000000..c7f4e03 --- /dev/null +++ b/full_home_tunnel.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +"""Migrate home tunnel to full local config on Proxmox host. +1. Build complete config.yml from cf-tunnels.md catalog +2. Install systemd unit, stop token-based duplicate, start local +Run ON Proxmox host as root. +""" +import io, subprocess, json + +CFG = "/etc/cloudflared/config.yml" +CRED = "/etc/cloudflared/home-tunnel.json" + +config = """tunnel: d2871458-1737-4844-b114-9a44b9d71e25 +credentials-file: {cred} +ingress: + - hostname: games.thetempleofdoom.com + service: http://10.30.20.73:3000 + - hostname: trustgram.thetempleofdoom.com + service: http://10.30.20.62:80 + - hostname: hydros.thetempleofdoom.com + service: http://10.30.20.11:3000 + - hostname: purehydro.thetempleofdoom.com + service: http://10.30.20.24:80 + - hostname: pve.thetempleofdoom.com + service: http://10.30.20.85:8006 + - hostname: gitea.thetempleofdoom.com + service: http://10.30.20.149:3000 + - hostname: wiki.thetempleofdoom.com + service: http://10.30.20.28:3000 + - hostname: CRM.thetempleofdoom.com + service: http://10.30.20.30:3000 + - hostname: crm.thetempleofdoom.com + service: http://10.30.20.30:3000 + - hostname: twenty.thetempleofdoom.com + service: http://10.30.20.30:3000 + - hostname: webcheck.thetempleofdoom.com + service: http://10.30.20.13:3000 + - hostname: mainagent.thetempleofdoom.com + service: http://10.30.20.236:5678 + - hostname: agent.thetempleofdoom.com + service: http://10.30.20.44:3000 + - hostname: btcpay.thetempleofdoom.com + service: http://10.30.20.140:80 + - hostname: btc.thetempleofdoom.com + service: http://10.30.20.140:80 + - hostname: torrent.thetempleofdoom.com + service: http://10.30.20.87:6969 + - hostname: jelly.thetempleofdoom.com + service: http://10.30.20.88:8096 + - hostname: spirit.thetempleofdoom.com + service: http://10.30.20.32:7777 + - hostname: share.thetempleofdoom.com + service: http://10.30.20.129:3000 + - hostname: snowshare.thetempleofdoom.com + service: http://10.30.20.129:3000 + - hostname: snowshoer.thetempleofdoom.com + service: http://10.30.20.129:3000 + - hostname: ai.thetempleofdoom.com + service: http://10.30.20.240:80 + - hostname: tech.thetempleofdoom.com + service: http://10.30.20.241:80 + - hostname: science.thetempleofdoom.com + service: http://10.30.20.242:80 + - hostname: crypto.thetempleofdoom.com + service: http://10.30.20.243:80 + - hostname: linux.thetempleofdoom.com + service: http://10.30.20.244:80 + - hostname: gaming.thetempleofdoom.com + service: http://10.30.20.246:80 + - hostname: diy.thetempleofdoom.com + service: http://10.30.20.247:80 + - hostname: guides.thetempleofdoom.com + service: http://10.30.20.248:80 + - hostname: digi-market.thetempleofdoom.com + service: http://10.30.20.210:5000 + - hostname: cardvault-pro.thetempleofdoom.com + service: http://10.30.20.211:5000 + - hostname: StreamPass-Hub.thetempleofdoom.com + service: http://10.30.20.212:5000 + - hostname: KeyForge-Digital.thetempleofdoom.com + service: http://10.30.20.213:5000 + - hostname: CoinBridgeMarket.thetempleofdoom.com + service: http://10.30.20.214:5000 + - hostname: GiftShift.thetempleofdoom.com + service: http://10.30.20.217:5000 + - hostname: PremiumPortal.thetempleofdoom.com + service: http://10.30.20.218:5000 + - hostname: DigiDeals.thetempleofdoom.com + service: http://10.30.20.219:5000 + - hostname: NexusKeys.thetempleofdoom.com + service: http://10.30.20.220:5000 + - hostname: VaultPass.thetempleofdoom.com + service: http://10.30.20.221:5000 + - hostname: PESTCONTROLLER.thetempleofdoom.com + service: http://10.30.20.249:5052 + - hostname: aisearch.thetempleofdoom.com + service: http://10.30.20.250:8000 + - hostname: signalsearcher.thetempleofdoom.com + service: http://10.30.20.23:5099 + - hostname: tarot.thetempleofdoom.com + service: http://10.30.20.77:80 + - hostname: socialpulse.thetempleofdoom.com + service: http://10.30.20.252:80 + - hostname: fmd.thetempleofdoom.com + service: http://10.30.20.181:8443 + - hostname: proxy.thetempleofdoom.com + service: http://10.30.20.154:3128 + - hostname: proxy2.thetempleofdoom.com + service: http://10.30.20.71:3128 + - hostname: proxy3.thetempleofdoom.com + service: http://10.30.20.189:3128 + - hostname: bwt.democracyrisingbwt.us + service: http://10.30.20.229:8008 + - service: http_status:404 +""".replace("{cred}", CRED) + +io.open(CFG, "w", encoding="utf-8").write(config) +print("CONFIG WRITTEN:", len(config.splitlines()), "ingress entries") + +unit = """[Unit] +Description=Cloudflare Home Tunnel (full fleet config) +After=network-online.target +Wants=network-online.target + +[Service] +ExecStart=/usr/bin/cloudflared tunnel --no-autoupdate --config /etc/cloudflared/config.yml run +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target +""" +io.open("/etc/systemd/system/cloudflared-home.service", "w", encoding="utf-8").write(unit) +subprocess.run(["systemctl", "daemon-reload"]) +print("UNIT INSTALLED") + +# stop the token-based home tunnel process (the d2871458 one) +out = subprocess.run(["bash", "-c", + "ps aux | grep cloudflared | grep -v grep | grep 'd2871458' | awk '{print $2}'"], + capture_output=True, text=True) +pids = out.stdout.split() +for p in pids: + subprocess.run(["kill", p]) +print("KILLED TOKEN-BASED HOME TUNNEL PIDS:", pids) + +subprocess.run(["systemctl", "enable", "cloudflared-home"]) +r = subprocess.run(["systemctl", "restart", "cloudflared-home"], capture_output=True, text=True) +print("LOCAL SERVICE STARTED" if r.returncode == 0 else f"START FAILED: {r.stderr[:400]}") diff --git a/home_tunnel.py b/home_tunnel.py new file mode 100644 index 0000000..bd72905 --- /dev/null +++ b/home_tunnel.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +"""Fix home tunnel config + install cloudflared-home systemd unit on CT148.""" +import io, subprocess + +# 1. backup + fix hydros ingress +subprocess.run(["cp", "/root/.cloudflared/config.yml", "/root/.cloudflared/config.yml.bak-20260813"]) +cfg = io.open("/root/.cloudflared/config.yml", encoding="utf-8").read() +cfg = cfg.replace("http://10.30.20.24:5000", "http://10.30.20.11:3000") +io.open("/root/.cloudflared/config.yml", "w", encoding="utf-8").write(cfg) +print("CONFIG FIXED (hydros -> .11:3000)") + +# 2. systemd unit +unit = """[Unit] +Description=Cloudflare Home Tunnel (games/trustgram/hydros) +After=network-online.target +Wants=network-online.target + +[Service] +ExecStart=/usr/bin/cloudflared tunnel --no-autoupdate --config /root/.cloudflared/config.yml run +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target +""" +io.open("/etc/systemd/system/cloudflared-home.service", "w", encoding="utf-8").write(unit) +subprocess.run(["systemctl", "daemon-reload"]) +subprocess.run(["systemctl", "enable", "cloudflared-home"]) +r = subprocess.run(["systemctl", "restart", "cloudflared-home"], capture_output=True, text=True) +print("SERVICE STARTED" if r.returncode == 0 else f"START FAILED: {r.stderr[:300]}") diff --git a/honest_online.py b/honest_online.py new file mode 100644 index 0000000..0671ab7 --- /dev/null +++ b/honest_online.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +"""Honest online counter for SatsSpin chat: real presence tracking + zero default.""" +import io + +# 1. Server: track chat pollers, return honest onlineUsersCount +P = "/opt/casino/server-prod-new.cjs" +src = io.open(P, encoding="utf-8").read() +old = 'app.get("/api/chat/messages", (req, res) => res.json({ messages: state.chat.slice(-50) }));' +new = '''const chatPresence = {}; +app.get("/api/chat/messages", (req, res) => { + const now = Date.now(); + const auth = req.headers.authorization || ""; + const tok = auth.startsWith("Bearer ") ? auth.slice(7) : ""; + const uid = tok && state.sessions && state.sessions[tok]; + const who = uid ? "u:" + uid : "ip:" + (req.ip || "?"); + chatPresence[who] = now; + for (const k in chatPresence) if (now - chatPresence[k] > 60000) delete chatPresence[k]; + res.json({ messages: state.chat.slice(-50), onlineUsersCount: Object.keys(chatPresence).length }); +});''' +assert old in src, "chat/messages handler not found" +src = src.replace(old, new, 1) +io.open(P, "w", encoding="utf-8").write(src) +print("SERVER PRESENCE ADDED") + +# 2. Frontend: no fake 432 default — start at 0 until the first real poll +B = "/opt/casino/dist/assets/index-CzzrlZy-.js" +bundle = io.open(B, encoding="utf-8").read() +old2 = "[A,v]=C.useState(432)" +assert old2 in bundle, "useState(432) not found" +bundle = bundle.replace(old2, "[A,v]=C.useState(0)", 1) +io.open(B, "w", encoding="utf-8").write(bundle) +print("BUNDLE DEFAULT ZEROED") diff --git a/liveness.py b/liveness.py new file mode 100644 index 0000000..be710e6 --- /dev/null +++ b/liveness.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Liveness-check every catalog backend from CT148 before DNS flip.""" +import subprocess, json + +# hostname -> (ip, port) +ROUTES = { + "pve": ("10.30.20.85", 8006), + "gitea": ("10.30.20.149", 3000), + "wiki": ("10.30.20.28", 3000), + "CRM": ("10.30.20.30", 3000), + "crm": ("10.30.20.30", 3000), + "twenty": ("10.30.20.30", 3000), + "webcheck": ("10.30.20.13", 3000), + "mainagent": ("10.30.20.236", 5678), + "agent": ("10.30.20.44", 3000), + "btcpay": ("10.30.20.140", 80), + "btc": ("10.30.20.140", 80), + "torrent": ("10.30.20.87", 6969), + "jelly": ("10.30.20.88", 8096), + "spirit": ("10.30.20.32", 7777), + "share": ("10.30.20.129", 3000), + "snowshare": ("10.30.20.129", 3000), + "snowshoer": ("10.30.20.129", 3000), + "ai": ("10.30.20.240", 80), + "tech": ("10.30.20.241", 80), + "science": ("10.30.20.242", 80), + "crypto": ("10.30.20.243", 80), + "linux": ("10.30.20.244", 80), + "gaming": ("10.30.20.246", 80), + "diy": ("10.30.20.247", 80), + "guides": ("10.30.20.248", 80), + "digi-market": ("10.30.20.210", 5000), + "cardvault-pro": ("10.30.20.211", 5000), + "StreamPass-Hub": ("10.30.20.212", 5000), + "KeyForge-Digital": ("10.30.20.213", 5000), + "CoinBridgeMarket": ("10.30.20.214", 5000), + "GiftShift": ("10.30.20.217", 5000), + "PremiumPortal": ("10.30.20.218", 5000), + "DigiDeals": ("10.30.20.219", 5000), + "NexusKeys": ("10.30.20.220", 5000), + "VaultPass": ("10.30.20.221", 5000), + "PESTCONTROLLER": ("10.30.20.249", 5052), + "aisearch": ("10.30.20.250", 8000), + "signalsearcher": ("10.30.20.23", 5099), + "tarot": ("10.30.20.77", 80), + "socialpulse": ("10.30.20.252", 80), + "fmd": ("10.30.20.181", 8443), + "proxy": ("10.30.20.154", 3128), + "proxy2": ("10.30.20.71", 3128), + "proxy3": ("10.30.20.189", 3128), + "purehydro": ("10.30.20.24", 80), + "bwt.democracyrisingbwt.us": ("10.30.20.229", 8008), +} + +dead = [] +for host, (ip, port) in ROUTES.items(): + r = subprocess.run(["curl", "-sk", "--max-time", "4", "-o", "/dev/null", "-w", "%{http_code}", + f"http://{ip}:{port}/"], capture_output=True, text=True) + code = r.stdout.strip() + ok = code and code != "000" and code != "404" + print(f"{host:24s} {ip}:{port} -> {code} {'OK' if ok else '*** CHECK ***'}") + if not ok: + dead.append((host, ip, port)) + +print() +print("DEAD/BAD:", dead if dead else "none — all backends answer") diff --git a/new_fleet_tunnel.py b/new_fleet_tunnel.py new file mode 100644 index 0000000..79e9105 --- /dev/null +++ b/new_fleet_tunnel.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +"""Create new cloudflared tunnel for trustgram/games/hydros on CT148 (cert-based, no API token).""" +import io, subprocess, re + +CF = "/root/.cloudflared" + +# 1. create tunnel +r = subprocess.run(["/usr/bin/cloudflared", "tunnel", "create", "thetemple-fleet"], + cwd=CF, capture_output=True, text=True, timeout=120) +print("CREATE:", r.stdout.strip()[-300:], r.stderr.strip()[-300:]) +m = re.search(r"Created tunnel\s+\w+\s+with id\s+([0-9a-f-]+)", r.stdout + r.stderr) +if not m: + m = re.search(r"([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})", r.stdout + r.stderr) +assert m, "tunnel UUID not found in output" +uuid = m.group(1) +print("UUID:", uuid) + +# 2. config +cfg = f"""tunnel: {uuid} +credentials-file: {CF}/{uuid}.json +ingress: + - hostname: trustgram.thetempleofdoom.com + service: http://10.30.20.62:80 + - hostname: games.thetempleofdoom.com + service: http://10.30.20.73:3000 + - hostname: hydros.thetempleofdoom.com + service: http://10.30.20.11:3000 + - service: http_status:404 +""" +io.open(f"{CF}/config-fleet.yml", "w", encoding="utf-8").write(cfg) +print("CONFIG WRITTEN") + +# 3. DNS routes +for host in ["trustgram", "games", "hydros"]: + d = host + ".thetempleofdoom.com" + rr = subprocess.run(["/usr/bin/cloudflared", "tunnel", "route", "dns", uuid, d], + cwd=CF, capture_output=True, text=True, timeout=60) + print(f"DNS {d}:", rr.stdout.strip()[-120:], rr.stderr.strip()[-120:]) + +# 4. systemd unit +unit = f"""[Unit] +Description=Cloudflare Fleet Tunnel (trustgram/games/hydros) +After=network-online.target +Wants=network-online.target + +[Service] +ExecStart=/usr/bin/cloudflared tunnel --no-autoupdate --config {CF}/config-fleet.yml run +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target +""" +io.open("/etc/systemd/system/cloudflared-fleet.service", "w", encoding="utf-8").write(unit) +subprocess.run(["systemctl", "daemon-reload"]) +subprocess.run(["systemctl", "enable", "cloudflared-fleet"]) +r = subprocess.run(["systemctl", "restart", "cloudflared-fleet"], capture_output=True, text=True) +print("SERVICE:", "STARTED" if r.returncode == 0 else r.stderr[:300]) diff --git a/patch_satsspin_play.py b/patch_satsspin_play.py new file mode 100644 index 0000000..25228f6 --- /dev/null +++ b/patch_satsspin_play.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +"""Add /play route to SatsSpin server (serves the real casino app shell).""" +import io + +P = "/opt/casino/server-prod-new.cjs" +src = io.open(P, encoding="utf-8").read() + +old = 'app.get("/{*splat}", (req, res) => res.sendFile(path.join(DIST, "index.html")));' +new = ('app.get("/play", (req, res) => res.sendFile(path.join(DIST, "app.html")));\n' + + old) +assert old in src, "catch-all route not found" +if 'app.get("/play"' not in src: + src = src.replace(old, new, 1) + io.open(P, "w", encoding="utf-8").write(src) + print("PATCHED") +else: + print("ALREADY PATCHED") + +# also fix the app shell title +A = "/opt/casino/dist/app.html" +app = io.open(A, encoding="utf-8").read() +app = app.replace("My Google AI Studio App", "SatsSpin — Casino") +io.open(A, "w", encoding="utf-8").write(app) +print("TITLE FIXED") diff --git a/patch_shim.py b/patch_shim.py new file mode 100644 index 0000000..ea6f666 --- /dev/null +++ b/patch_shim.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +"""Add double-load guard to SatsSpin auth shim (prevents fetch recursion).""" +import io + +P = "/opt/casino/dist/shim.js" +src = io.open(P, encoding="utf-8").read() +guard = " if (window.__satsspinShimLoaded) { return; }\n window.__satsspinShimLoaded = true;\n" +old = "(function () {\n 'use strict';\n" +assert old in src, "IIFE start not found" +if "__satsspinShimLoaded" not in src: + src = src.replace(old, old + guard, 1) + io.open(P, "w", encoding="utf-8").write(src) + print("SHIM GUARDED") +else: + print("ALREADY GUARDED") diff --git a/remove_dup_leaderboard.py b/remove_dup_leaderboard.py new file mode 100644 index 0000000..dfe7903 --- /dev/null +++ b/remove_dup_leaderboard.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +"""Remove duplicate Leaderboard button (wallet row) from SatsSpin bundle.""" +import io + +B = "/opt/casino/dist/assets/index-CzzrlZy-.js" +bundle = io.open(B, encoding="utf-8").read() + +old = ('s.jsxs("button",{onClick:()=>fe(!0),className:"flex items-center gap-1.5 px-3 py-2 ' + 'rounded-xl bg-amber-500/10 hover:bg-amber-500/20 text-amber-400 border ' + 'border-amber-500/30 text-xs font-bold transition-all cursor-pointer",' + 'children:[s.jsx(xi,{className:"w-3.5 h-3.5"}),s.jsx("span",{className:"hidden ' + 'sm:inline",children:"Leaderboard"})]}),') + +n = bundle.count(old) +assert n == 1, f"expected 1 match, found {n}" +bundle = bundle.replace(old, "", 1) +io.open(B, "w", encoding="utf-8").write(bundle) +print("DUPLICATE LEADERBOARD BUTTON REMOVED") diff --git a/rewrite_shim.py b/rewrite_shim.py new file mode 100644 index 0000000..3886fdd --- /dev/null +++ b/rewrite_shim.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +"""Rewrite SatsSpin shim fetch patch: re-entrancy-proof via global original fetch.""" +import io, re + +P = "/opt/casino/dist/shim.js" +src = io.open(P, encoding="utf-8").read() + +old_patch = """ // 1. Patch fetch to inject Authorization header. + var _fetch = window.fetch; + window.fetch = function (url, opts) { + opts = opts || {}; + opts.headers = opts.headers || {}; + if (TOKEN && !opts.headers['Authorization']) { + opts.headers['Authorization'] = 'Bearer ' + TOKEN; + } + return _fetch.call(this, url, opts).then(function (r) { + if (r.status === 401) { TOKEN = ''; USER_ID = ''; localStorage.removeItem('satsspin_token'); } + return r; + }); + }; +""" +new_patch = """ // 1. Patch fetch to inject Authorization header. Re-entrancy-proof: + // the ORIGINAL native fetch is captured once on window — even if this + // script somehow executes twice, both wrappers call the same original. + if (!window.__ssOriginalFetch) { + window.__ssOriginalFetch = window.fetch.bind(window); + } + var _fetch = window.__ssOriginalFetch; + window.fetch = function (url, opts) { + opts = opts || {}; + opts.headers = opts.headers || {}; + if (TOKEN && !opts.headers['Authorization']) { + opts.headers['Authorization'] = 'Bearer ' + TOKEN; + } + return _fetch(url, opts).then(function (r) { + if (r.status === 401) { TOKEN = ''; USER_ID = ''; localStorage.removeItem('satsspin_token'); } + return r; + }); + }; +""" +assert old_patch in src, "old fetch patch not found" +src = src.replace(old_patch, new_patch, 1) +io.open(P, "w", encoding="utf-8").write(src) + +# cache-bust the script tag in app.html +A = "/opt/casino/dist/app.html" +app = io.open(A, encoding="utf-8").read() +app = app.replace('', '') +io.open(A, "w", encoding="utf-8").write(app) +print("SHIM REWRITTEN + CACHE-BUSTED") diff --git a/takeover.py b/takeover.py new file mode 100644 index 0000000..c4faf6a --- /dev/null +++ b/takeover.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +"""Take over all healthy routes onto thetemple-fleet tunnel (CT148).""" +import io, subprocess + +UUID = "1aeb1ac0-3c49-4801-b61d-bc92e6e4932a" +CF = "/root/.cloudflared" + +# verified-live routes (liveness sweep 2026-08-13); problem routes stay on old tunnel +ROUTES = [ + ("pve.thetempleofdoom.com", "http://10.30.20.85:8006"), + ("gitea.thetempleofdoom.com", "http://10.30.20.149:3000"), + ("wiki.thetempleofdoom.com", "http://10.30.20.28:3000"), + ("CRM.thetempleofdoom.com", "http://10.30.20.30:3000"), + ("crm.thetempleofdoom.com", "http://10.30.20.30:3000"), + ("twenty.thetempleofdoom.com", "http://10.30.20.30:3000"), + ("webcheck.thetempleofdoom.com", "http://10.30.20.13:3000"), + ("agent.thetempleofdoom.com", "http://10.30.20.44:3000"), + ("btcpay.thetempleofdoom.com", "http://10.30.20.140:80"), + ("btc.thetempleofdoom.com", "http://10.30.20.140:80"), + ("torrent.thetempleofdoom.com", "http://10.30.20.87:6969"), + ("jelly.thetempleofdoom.com", "http://10.30.20.88:8096"), + ("spirit.thetempleofdoom.com", "http://10.30.20.32:7777"), + ("share.thetempleofdoom.com", "http://10.30.20.129:3000"), + ("snowshare.thetempleofdoom.com", "http://10.30.20.129:3000"), + ("snowshoer.thetempleofdoom.com", "http://10.30.20.129:3000"), + ("ai.thetempleofdoom.com", "http://10.30.20.240:80"), + ("tech.thetempleofdoom.com", "http://10.30.20.241:80"), + ("science.thetempleofdoom.com", "http://10.30.20.242:80"), + ("crypto.thetempleofdoom.com", "http://10.30.20.243:80"), + ("linux.thetempleofdoom.com", "http://10.30.20.244:80"), + ("gaming.thetempleofdoom.com", "http://10.30.20.246:80"), + ("diy.thetempleofdoom.com", "http://10.30.20.247:80"), + ("guides.thetempleofdoom.com", "http://10.30.20.248:80"), + ("digi-market.thetempleofdoom.com", "http://10.30.20.210:5000"), + ("cardvault-pro.thetempleofdoom.com", "http://10.30.20.211:5000"), + ("StreamPass-Hub.thetempleofdoom.com", "http://10.30.20.212:5000"), + ("KeyForge-Digital.thetempleofdoom.com", "http://10.30.20.213:5000"), + ("CoinBridgeMarket.thetempleofdoom.com", "http://10.30.20.214:5000"), + ("GiftShift.thetempleofdoom.com", "http://10.30.20.217:5000"), + ("PremiumPortal.thetempleofdoom.com", "http://10.30.20.218:5000"), + ("DigiDeals.thetempleofdoom.com", "http://10.30.20.219:5000"), + ("NexusKeys.thetempleofdoom.com", "http://10.30.20.220:5000"), + ("VaultPass.thetempleofdoom.com", "http://10.30.20.221:5000"), + ("PESTCONTROLLER.thetempleofdoom.com", "http://10.30.20.249:5052"), + ("aisearch.thetempleofdoom.com", "http://10.30.20.250:8000"), + ("signalsearcher.thetempleofdoom.com", "http://10.30.20.23:5099"), + ("tarot.thetempleofdoom.com", "http://10.30.20.77:80"), + ("socialpulse.thetempleofdoom.com", "http://10.30.20.252:80"), + ("proxy.thetempleofdoom.com", "http://10.30.20.154:3128"), + ("proxy2.thetempleofdoom.com", "http://10.30.20.71:3128"), + ("proxy3.thetempleofdoom.com", "http://10.30.20.189:3128"), + ("trustgram.thetempleofdoom.com", "http://10.30.20.62:80"), + ("games.thetempleofdoom.com", "http://10.30.20.73:3000"), + ("hydros.thetempleofdoom.com", "http://10.30.20.11:3000"), + ("bwt.democracyrisingbwt.us", "http://10.30.20.229:8008"), +] + +# build config +ingress = "\n".join(f" - hostname: {h}\n service: {s}" for h, s in ROUTES) +cfg = f"""tunnel: {UUID} +credentials-file: {CF}/{UUID}.json +ingress: +{ingress} + - service: http_status:404 +""" +io.open(f"{CF}/config-fleet.yml", "w", encoding="utf-8").write(cfg) +print(f"CONFIG: {len(ROUTES)} routes written") +subprocess.run(["systemctl", "restart", "cloudflared-fleet"]) +print("TUNNEL RESTARTED") + +# flip DNS +fails = [] +for h, _ in ROUTES: + r = subprocess.run(["/usr/bin/cloudflared", "tunnel", "route", "dns", "--overwrite-dns", UUID, h], + cwd=CF, capture_output=True, text=True, timeout=60) + ok = "Added CNAME" in (r.stdout + r.stderr) + print(f"{'OK ' if ok else 'FAIL'} {h}") + if not ok: + fails.append(h) +print("DNS FAILS:", fails if fails else "none")