Snapshot: full project state
This commit is contained in:
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
node_modules/
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
.env
|
||||||
|
*.db
|
||||||
|
*.sqlite*
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
out/
|
||||||
|
work/
|
||||||
|
.pio/
|
||||||
|
briefs/
|
||||||
|
dns-backup/
|
||||||
|
archive/
|
||||||
|
*.png
|
||||||
10
README.md
Normal file
10
README.md
Normal file
@@ -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/<site>.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.
|
||||||
|
|
||||||
314
build.py
Normal file
314
build.py
Normal file
@@ -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/<site>.html
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
|
||||||
|
OUT = os.path.expanduser("~/landing-factory/out")
|
||||||
|
os.makedirs(OUT, exist_ok=True)
|
||||||
|
|
||||||
|
ENGINE_CSS = """
|
||||||
|
<style>
|
||||||
|
:root{--accent:AC;--accent2:AC2;--bg:BG;--bg2:BG2;--card:CARD;--text:TXT;--muted:MUT}
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
html{scroll-behavior:smooth}
|
||||||
|
body{background:var(--bg);color:var(--text);font-family:'Segoe UI',system-ui,-apple-system,sans-serif;overflow-x:hidden;line-height:1.6}
|
||||||
|
::selection{background:var(--accent);color:#000}
|
||||||
|
#fx{position:fixed;inset:0;z-index:0;pointer-events:none}
|
||||||
|
.wrap{position:relative;z-index:1;max-width:1180px;margin:0 auto;padding:0 24px}
|
||||||
|
nav{position:fixed;top:0;left:0;right:0;z-index:50;display:flex;align-items:center;justify-content:space-between;padding:16px 28px;background:rgba(0,0,0,.35);backdrop-filter:blur(14px);border-bottom:1px solid rgba(255,255,255,.06)}
|
||||||
|
.brand{display:flex;align-items:center;gap:10px;font-weight:800;font-size:1.15rem;letter-spacing:.5px}
|
||||||
|
.brand .dot{width:10px;height:10px;border-radius:50%;background:var(--accent);box-shadow:0 0 14px var(--accent);animation:pulse 2s infinite}
|
||||||
|
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
|
||||||
|
.navlinks{display:flex;gap:22px;align-items:center}
|
||||||
|
.navlinks a{color:var(--muted);text-decoration:none;font-size:.92rem;font-weight:600;transition:.25s}
|
||||||
|
.navlinks a:hover{color:var(--text)}
|
||||||
|
.btn{display:inline-block;padding:14px 30px;border-radius:14px;font-weight:800;font-size:1rem;letter-spacing:.3px;cursor:pointer;border:none;text-decoration:none;transition:transform .25s,box-shadow .25s;position:relative;overflow:hidden}
|
||||||
|
.btn:hover{transform:translateY(-3px)}
|
||||||
|
.btn-primary{background:linear-gradient(135deg,var(--accent),var(--accent2));color:#04101f;box-shadow:0 8px 30px COLORGLOW}
|
||||||
|
.btn-primary:hover{box-shadow:0 14px 44px COLORGLOW}
|
||||||
|
.btn-ghost{background:rgba(255,255,255,.06);color:var(--text);border:1px solid rgba(255,255,255,.16)}
|
||||||
|
.btn-ghost:hover{background:rgba(255,255,255,.12)}
|
||||||
|
.hero{min-height:100vh;display:flex;flex-direction:column;justify-content:center;align-items:center;text-align:center;padding:120px 24px 60px;position:relative}
|
||||||
|
.badge{display:inline-flex;align-items:center;gap:8px;padding:8px 18px;border-radius:40px;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.12);font-size:.85rem;font-weight:700;color:var(--muted);margin-bottom:22px;letter-spacing:.4px}
|
||||||
|
.badge .live{width:8px;height:8px;border-radius:50%;background:#2fe07b;box-shadow:0 0 10px #2fe07b;animation:pulse 1.6s infinite}
|
||||||
|
.hero h1{font-size:clamp(2.6rem,7vw,5.6rem);font-weight:900;line-height:1.05;letter-spacing:-1.5px;background:linear-gradient(120deg,#fff 20%,var(--accent) 55%,var(--accent2) 90%);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;margin:10px 0 20px;filter:drop-shadow(0 0 34px COLORGLOW)}
|
||||||
|
.hero .sub{max-width:640px;color:var(--muted);font-size:1.15rem;margin-bottom:36px}
|
||||||
|
.hero .cta{display:flex;gap:16px;flex-wrap:wrap;justify-content:center}
|
||||||
|
.typewrap{font-family:'SF Mono',ui-monospace,Menlo,monospace;color:var(--accent);font-size:1.05rem;font-weight:700;margin-bottom:14px;min-height:28px}
|
||||||
|
.typewrap .cursor{animation:blink 1s steps(1) infinite}
|
||||||
|
@keyframes blink{50%{opacity:0}}
|
||||||
|
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:18px;margin:80px auto 0;width:100%;max-width:900px}
|
||||||
|
.stat{padding:22px;border-radius:18px;background:linear-gradient(160deg,rgba(255,255,255,.06),rgba(255,255,255,.02));border:1px solid rgba(255,255,255,.08);backdrop-filter:blur(8px)}
|
||||||
|
.stat .n{font-size:2.3rem;font-weight:900;background:linear-gradient(135deg,var(--accent),var(--accent2));-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}
|
||||||
|
.stat .l{color:var(--muted);font-size:.85rem;font-weight:600;margin-top:4px}
|
||||||
|
section{padding:90px 0;position:relative}
|
||||||
|
section h2{font-size:clamp(1.9rem,4vw,3rem);font-weight:900;text-align:center;letter-spacing:-.5px;margin-bottom:12px}
|
||||||
|
section .lead{text-align:center;color:var(--muted);max-width:620px;margin:0 auto 50px;font-size:1.05rem}
|
||||||
|
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:22px}
|
||||||
|
.card{background:linear-gradient(160deg,rgba(255,255,255,.05),rgba(255,255,255,.015));border:1px solid rgba(255,255,255,.09);border-radius:20px;padding:28px;transition:transform .3s,border-color .3s,box-shadow .3s;transform-style:preserve-3d;will-change:transform}
|
||||||
|
.card:hover{border-color:ACBORDER;box-shadow:0 20px 60px COLORGLOW_SOFT;transform:translateY(-6px)}
|
||||||
|
.card .ico{font-size:2.4rem;margin-bottom:14px;display:inline-block}
|
||||||
|
.card h3{font-size:1.25rem;margin-bottom:8px}
|
||||||
|
.card p{color:var(--muted);font-size:.95rem}
|
||||||
|
.stepnum{font-size:1rem;font-weight:900;color:var(--accent);letter-spacing:1px}
|
||||||
|
.marquee{overflow:hidden;border-top:1px solid rgba(255,255,255,.07);border-bottom:1px solid rgba(255,255,255,.07);padding:16px 0;background:rgba(0,0,0,.25);margin:30px 0}
|
||||||
|
.marquee .track{display:flex;gap:48px;white-space:nowrap;animation:slide 30s linear infinite;width:max-content;font-weight:800;font-size:1.05rem;color:var(--muted)}
|
||||||
|
.marquee .track span i{color:var(--accent);font-style:normal;margin-right:8px}
|
||||||
|
@keyframes slide{to{transform:translateX(-50%)}}
|
||||||
|
.pricing{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:22px;max-width:900px;margin:0 auto}
|
||||||
|
.price-card{border-radius:22px;padding:36px 28px;text-align:center;border:1px solid rgba(255,255,255,.1);background:rgba(255,255,255,.03);transition:.3s;position:relative}
|
||||||
|
.price-card.hot{border-color:var(--accent);box-shadow:0 0 50px COLORGLOW_SOFT;transform:scale(1.03)}
|
||||||
|
.price-card .tag{position:absolute;top:-13px;left:50%;transform:translateX(-50%);background:linear-gradient(135deg,var(--accent),var(--accent2));color:#04101f;font-size:.75rem;font-weight:900;padding:4px 16px;border-radius:20px;letter-spacing:1px}
|
||||||
|
.price-card .amount{font-size:3rem;font-weight:900;margin:14px 0 4px}
|
||||||
|
.price-card .per{color:var(--muted);font-size:.9rem;margin-bottom:20px}
|
||||||
|
.price-card ul{list-style:none;text-align:left;margin:0 auto 26px;max-width:260px}
|
||||||
|
.price-card li{padding:7px 0;color:var(--muted);font-size:.95rem;border-bottom:1px dashed rgba(255,255,255,.08)}
|
||||||
|
.price-card li::before{content:"✓ ";color:var(--accent);font-weight:900}
|
||||||
|
.cta-band{text-align:center;padding:80px 24px;border-radius:28px;background:radial-gradient(ellipse at center,COLORGLOW_BAND,transparent 70%),rgba(255,255,255,.02);border:1px solid rgba(255,255,255,.08);margin:0 auto;max-width:1000px}
|
||||||
|
.cta-band h2{font-size:clamp(2rem,4.5vw,3.2rem)}
|
||||||
|
footer{padding:44px 24px;border-top:1px solid rgba(255,255,255,.07);text-align:center;color:var(--muted);font-size:.9rem;margin-top:60px}
|
||||||
|
footer a{color:var(--accent);text-decoration:none;font-weight:700}
|
||||||
|
.reveal{opacity:0;transform:translateY(36px);transition:opacity .8s cubic-bezier(.16,1,.3,1),transform .8s cubic-bezier(.16,1,.3,1)}
|
||||||
|
.reveal.in{opacity:1;transform:none}
|
||||||
|
#stickycta{position:fixed;bottom:20px;left:50%;transform:translate(-50%,120px);z-index:60;transition:transform .4s cubic-bezier(.16,1,.3,1)}
|
||||||
|
#stickycta.show{transform:translate(-50%,0)}
|
||||||
|
@media (max-width:760px){.navlinks{display:none}.hero{padding-top:90px}.stats{grid-template-columns:repeat(2,1fr)}}
|
||||||
|
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||||
|
</style>
|
||||||
|
"""
|
||||||
|
|
||||||
|
ENGINE_JS = """
|
||||||
|
<script>
|
||||||
|
(function(){
|
||||||
|
var cfg={accent:"AC",accent2:"AC2",bg2:"BG2",glow:"COLORGLOW",wave:WAVE,slots:SLOTS,typephrases:TYPEPHRASES};
|
||||||
|
// marquee duplication for seamless loop
|
||||||
|
var mt=document.getElementById('mtrack');if(mt){mt.innerHTML+=mt.innerHTML;}
|
||||||
|
// particle field
|
||||||
|
var c=document.getElementById('fx'),x=c.getContext('2d'),W,H,pts=[],mouse={x:-9e3,y:-9e3};
|
||||||
|
function rs(){W=c.width=innerWidth;H=c.height=innerHeight;var n=Math.min(110,Math.floor(W*H/16000));pts=[];for(var i=0;i<n;i++)pts.push({x:Math.random()*W,y:Math.random()*H,vx:(Math.random()-.5)*.5,vy:(Math.random()-.5)*.5,r:Math.random()*1.8+.6});}
|
||||||
|
rs();addEventListener('resize',rs);addEventListener('mousemove',function(e){mouse.x=e.clientX;mouse.y=e.clientY});
|
||||||
|
function tick(){x.clearRect(0,0,W,H);for(var i=0;i<pts.length;i++){var p=pts[i];p.x+=p.vx;p.y+=p.vy;if(p.x<0||p.x>W)p.vx*=-1;if(p.y<0||p.y>H)p.vy*=-1;
|
||||||
|
var dx=p.x-mouse.x,dy=p.y-mouse.y,d2=dx*dx+dy*dy;if(d2<14400){p.x+=dx*.02;p.y+=dy*.02}
|
||||||
|
x.beginPath();x.arc(p.x,p.y,p.r,0,6.29);x.fillStyle=cfg.accent;x.globalAlpha=.55;x.fill();
|
||||||
|
for(var j=i+1;j<pts.length;j++){var q=pts[j],ddx=p.x-q.x,ddy=p.y-q.y,dd=ddx*ddx+ddy*ddy;if(dd<12000){x.globalAlpha=.16*(1-dd/12000);x.beginPath();x.moveTo(p.x,p.y);x.lineTo(q.x,q.y);x.strokeStyle=cfg.accent;x.lineWidth=.7;x.stroke();}}}
|
||||||
|
x.globalAlpha=1;requestAnimationFrame(tick)}tick();
|
||||||
|
// typewriter
|
||||||
|
var tw=document.getElementById('typewriter'),ph=cfg.typephrases,i=0,ci=0,del=false;
|
||||||
|
function type(){var t=ph[i];ci+=del?-1:1;tw.textContent=t.slice(0,ci);var sp=del?28:52;
|
||||||
|
if(!del&&ci===t.length){sp=1700;del=true}else if(del&&ci===0){del=false;i=(i+1)%ph.length;sp=300}
|
||||||
|
setTimeout(type,sp)}if(tw)type();
|
||||||
|
// reveals + countup
|
||||||
|
var io=new IntersectionObserver(function(es){es.forEach(function(e){if(e.isIntersecting){e.target.classList.add('in');var ns=e.target.querySelectorAll('[data-count]');ns.forEach(function(n){var end=parseFloat(n.getAttribute('data-count')),t0=null;
|
||||||
|
function up(ts){if(!t0)t0=ts;var p=Math.min((ts-t0)/1600,1);n.textContent=Math.round(end*(1-Math.pow(1-p,3))).toLocaleString();if(p<1)requestAnimationFrame(up)}requestAnimationFrame(up)});io.unobserve(e.target)}})},{threshold:.15});
|
||||||
|
document.querySelectorAll('.reveal').forEach(function(el){io.observe(el)});
|
||||||
|
// tilt
|
||||||
|
document.querySelectorAll('.tilt').forEach(function(t){t.addEventListener('mousemove',function(e){var r=t.getBoundingClientRect(),px=(e.clientX-r.left)/r.width-.5,py=(e.clientY-r.top)/r.height-.5;t.style.transform='perspective(900px) rotateY('+(px*10)+'deg) rotateX('+(-py*10)+'deg) translateY(-6px)'});t.addEventListener('mouseleave',function(){t.style.transform=''});});
|
||||||
|
// sticky cta
|
||||||
|
var st=document.getElementById('stickycta');if(st)addEventListener('scroll',function(){st.classList.toggle('show',scrollY>500)});
|
||||||
|
// waveform (signal miner)
|
||||||
|
var wv=document.getElementById('wave');if(wv){var wx=wv.getContext('2d');var wd=[];for(var k=0;k<180;k++)wd.push(Math.random());
|
||||||
|
function wtick(){wx.clearRect(0,0,wv.width,wv.height);wx.beginPath();for(k=0;k<wd.length;k++){var a=(wd[k]*.5+.25)*wv.height;var px=k/wd.length*wv.width;k===0?wx.moveTo(px,a):wx.lineTo(px,a);wd[k]=Math.max(0,Math.min(1,wd[k]+(Math.random()-.5)*.14))}wx.strokeStyle=cfg.accent;wx.lineWidth=2.5;wx.shadowColor=cfg.accent;wx.shadowBlur=14;wx.stroke();requestAnimationFrame(wtick)}wtick();}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
"""
|
||||||
|
|
||||||
|
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="""<div class="hero">
|
||||||
|
<div class="typewrap"><span id="typewriter"></span><span class="cursor">▌</span></div>
|
||||||
|
<div class="badge"><span class="live"></span> LIVE · Lightning Network Casino</div>
|
||||||
|
<h1>Spin The Lightning.</h1>
|
||||||
|
<p class="sub">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.</p>
|
||||||
|
<div class="cta"><a class="btn btn-primary" href="/play">⚡ Play Now</a><a class="btn btn-ghost" href="#how">How It Works</a></div>
|
||||||
|
<div class="stats reveal">
|
||||||
|
<div class="stat"><div class="n" data-count="6">0</div><div class="l">Casino Games</div></div>
|
||||||
|
<div class="stat"><div class="n" data-count="100">0</div><div class="l">% Lightning Payouts</div></div>
|
||||||
|
<div class="stat"><div class="n" data-count="0">0</div><div class="l">KYC Requirements</div></div>
|
||||||
|
<div class="stat"><div class="n" data-count="24">0</div><div class="l">/ 7 Live</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="marquee"><div class="track" id="mtrack"><span><i>🎰</i>SLOTS</span><span><i>💣</i>MINES</span><span><i>🎲</i>DICE</span><span><i>📈</i>CRASH</span><span><i>🃏</i>BLACKJACK</span><span><i>🎯</i>PLINKO</span><span><i>⚡</i>LIGHTNING</span></div></div>
|
||||||
|
</div>
|
||||||
|
<section id="games"><div class="wrap">
|
||||||
|
<h2 class="reveal">Pick Your Poison</h2>
|
||||||
|
<p class="lead reveal">Six games. One engine. Zero delays. <a href="/play" style="color:var(--accent);font-weight:800">Launch the casino →</a></p>
|
||||||
|
<div class="grid">
|
||||||
|
<div class="card tilt reveal"><span class="ico">🎰</span><h3>Slots</h3><p>Classic 3-reel action with 777 jackpots and multiplier lines. Pure dopamine, one spin away.</p></div>
|
||||||
|
<div class="card tilt reveal"><span class="ico">💣</span><h3>Mines</h3><p>Reveal safe tiles, dodge the bombs, cash out when your nerve breaks. Risk/reward at its finest.</p></div>
|
||||||
|
<div class="card tilt reveal"><span class="ico">🎲</span><h3>Dice</h3><p>Pick your odds, roll under the number, double or nothing. The simplest game in the house.</p></div>
|
||||||
|
<div class="card tilt reveal"><span class="ico">📈</span><h3>Crash</h3><p>The multiplier climbs until it doesn't. Cash out before the crash — greed is a strategy, until it isn't.</p></div>
|
||||||
|
<div class="card tilt reveal"><span class="ico">🃏</span><h3>Blackjack</h3><p>Beat the dealer to 21. Hit, stand, and count your sats when the house busts.</p></div>
|
||||||
|
<div class="card tilt reveal"><span class="ico">🎯</span><h3>Plinko</h3><p>Drop the ball, watch it bounce, pray to the peg gods. Big drops mean big multipliers.</p></div>
|
||||||
|
</div>
|
||||||
|
</div></section>
|
||||||
|
<section id="how"><div class="wrap">
|
||||||
|
<h2 class="reveal">Three Steps To Winning</h2>
|
||||||
|
<p class="lead reveal">Faster than a bank transfer to the moon.</p>
|
||||||
|
<div class="grid">
|
||||||
|
<div class="card reveal"><div class="stepnum">01</div><h3>Create a House Account</h3><p>No KYC, no email hoops. Your account lives in the casino — anonymous by design.</p></div>
|
||||||
|
<div class="card reveal"><div class="stepnum">02</div><h3>⚡ Top Up With Lightning</h3><p>Scan the invoice with any Lightning wallet. Sats land in seconds, not days.</p></div>
|
||||||
|
<div class="card reveal"><div class="stepnum">03</div><h3>Play & Withdraw Instantly</h3><p>Bet sats, stack sats, withdraw sats. Payouts hit your wallet the moment you cash out.</p></div>
|
||||||
|
</div>
|
||||||
|
</div></section>
|
||||||
|
<section id="lightning"><div class="wrap">
|
||||||
|
<div class="cta-band reveal">
|
||||||
|
<h2>⚡ Powered By The Lightning Network</h2>
|
||||||
|
<p class="lead" style="margin:14px auto 34px">Real Bitcoin, real fast. Deposits and withdrawals over Lightning — the same network the whole sats economy runs on.</p>
|
||||||
|
<a class="btn btn-primary" href="/play">⚡ Enter The Casino</a>
|
||||||
|
</div>
|
||||||
|
</div></section>
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
"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="""<div class="hero">
|
||||||
|
<div class="typewrap"><span id="typewriter"></span><span class="cursor">▌</span></div>
|
||||||
|
<div class="badge"><span class="live"></span> LIVE · 1,085+ Telegram Marketplaces</div>
|
||||||
|
<h1>Mine The Signal.<br>Trade The Noise.</h1>
|
||||||
|
<p class="sub">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.</p>
|
||||||
|
<div class="cta"><a class="btn btn-primary" href="/signup">Get Free Searches</a><a class="btn btn-ghost" href="#pricing">$1 = 5 Searches</a></div>
|
||||||
|
<canvas id="wave" width="900" height="120" style="width:100%;max-width:900px;margin-top:60px;display:block"></canvas>
|
||||||
|
<div class="stats reveal">
|
||||||
|
<div class="stat"><div class="n" data-count="1085">0</div><div class="l">Channels Monitored</div></div>
|
||||||
|
<div class="stat"><div class="n" data-count="776">0</div><div class="l">Commerce Categories</div></div>
|
||||||
|
<div class="stat"><div class="n" data-count="430">0</div><div class="l">Gift Card Listings</div></div>
|
||||||
|
<div class="stat"><div class="n" data-count="0">0</div><div class="l">KYC Checks</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<section id="features"><div class="wrap">
|
||||||
|
<h2 class="reveal">Built For Deal Hunters</h2>
|
||||||
|
<p class="lead reveal">Everything you need to find the signal in the noise.</p>
|
||||||
|
<div class="grid">
|
||||||
|
<div class="card tilt reveal"><span class="ico">🔍</span><h3>Instant Search</h3><p>Search every monitored channel for the exact deal you want. Results in seconds, not scrobbles.</p></div>
|
||||||
|
<div class="card tilt reveal"><span class="ico">🎯</span><h3>Credibility Scoring</h3><p>Every seller and channel gets a trust score built from history, behavior and network signals.</p></div>
|
||||||
|
<div class="card tilt reveal"><span class="ico">🤖</span><h3>API Access</h3><p>Wire Signal Miner into your own bots and alerting. Your API key, your pipeline.</p></div>
|
||||||
|
<div class="card tilt reveal"><span class="ico">🛒</span><h3>776 Categories</h3><p>Gift cards, software keys, accounts, hardware, services. If it's traded, it's tracked.</p></div>
|
||||||
|
<div class="card tilt reveal"><span class="ico">⚡</span><h3>Real-Time Feeds</h3><p>New listings land the moment they're posted. Speed is the entire game.</p></div>
|
||||||
|
<div class="card tilt reveal"><span class="ico">🔓</span><h3>No KYC, Ever</h3><p>Just an email for your key. Pay in Bitcoin. No forms, no ID, no nonsense.</p></div>
|
||||||
|
</div>
|
||||||
|
</div></section>
|
||||||
|
<section id="coverage"><div class="wrap">
|
||||||
|
<h2 class="reveal">The Underground, Mapped</h2>
|
||||||
|
<p class="lead reveal">1,085+ channels across 776 commerce categories — and counting.</p>
|
||||||
|
<div class="grid">
|
||||||
|
<div class="card reveal" style="text-align:center"><span class="ico">🎁</span><div class="n" data-count="430" style="font-size:2.2rem;font-weight:900;color:var(--accent)">0</div><p>Gift Cards</p></div>
|
||||||
|
<div class="card reveal" style="text-align:center"><span class="ico">🔑</span><div class="n" data-count="60" style="font-size:2.2rem;font-weight:900;color:var(--accent)">0</div><p>Software Keys</p></div>
|
||||||
|
<div class="card reveal" style="text-align:center"><span class="ico">📺</span><div class="n" data-count="86" style="font-size:2.2rem;font-weight:900;color:var(--accent)">0</div><p>Accounts</p></div>
|
||||||
|
<div class="card reveal" style="text-align:center"><span class="ico">💻</span><div class="n" data-count="80" style="font-size:2.2rem;font-weight:900;color:var(--accent)">0</div><p>Hardware</p></div>
|
||||||
|
<div class="card reveal" style="text-align:center"><span class="ico">🛠️</span><div class="n" data-count="120" style="font-size:2.2rem;font-weight:900;color:var(--accent)">0</div><p>Services & Tools</p></div>
|
||||||
|
</div>
|
||||||
|
</div></section>
|
||||||
|
<section id="pricing"><div class="wrap">
|
||||||
|
<h2 class="reveal">Ridiculously Simple Pricing</h2>
|
||||||
|
<p class="lead reveal">No subscriptions. Pay with Bitcoin. Get intelligence.</p>
|
||||||
|
<div class="pricing">
|
||||||
|
<div class="price-card reveal"><h3>🆓 Free Tier</h3><div class="amount">$0</div><div class="per">forever</div>
|
||||||
|
<ul><li>Free starter searches</li><li>Full channel access</li><li>Credibility scoring</li><li>No expiration</li></ul>
|
||||||
|
<a class="btn btn-ghost" href="/signup">Start Free</a></div>
|
||||||
|
<div class="price-card hot reveal"><div class="tag">MOST POPULAR</div><h3>🚀 Researcher Tier</h3><div class="amount">$1</div><div class="per">= 5 searches · Bitcoin ⚡</div>
|
||||||
|
<ul><li>5 marketplace searches</li><li>Full access to 1,085+ channels</li><li>Credibility scoring</li><li>API access for bots</li><li>No expiration · No KYC</li></ul>
|
||||||
|
<a class="btn btn-primary" href="/signup">Sign Up Free</a></div>
|
||||||
|
</div>
|
||||||
|
</div></section>
|
||||||
|
<section><div class="wrap">
|
||||||
|
<div class="cta-band reveal">
|
||||||
|
<h2>The Next Deal Is Already Posted.</h2>
|
||||||
|
<p class="lead" style="margin:14px auto 34px">Get your free searches and see what the underground is selling today.</p>
|
||||||
|
<a class="btn btn-primary" href="/signup">Claim Free Searches</a>
|
||||||
|
</div>
|
||||||
|
</div></section>
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
"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="""<div class="hero">
|
||||||
|
<div class="typewrap"><span id="typewriter"></span><span class="cursor">▌</span></div>
|
||||||
|
<div class="badge"><span class="live"></span> CERTUS · KNOWLEDGE ENGINE</div>
|
||||||
|
<h1>Your Second Brain<br>For Pest Control.</h1>
|
||||||
|
<p class="sub">WorkBrain ingests your documents, chunks them into knowledge, and answers questions with citations. Chat, search, and squash bugs — one brain for the whole operation.</p>
|
||||||
|
<div class="cta"><a class="btn btn-primary" href="/app">🧠 Open WorkBrain</a><a class="btn btn-ghost" href="#features">What It Does</a></div>
|
||||||
|
<div class="stats reveal">
|
||||||
|
<div class="stat"><div class="n" data-count="24">0</div><div class="l">/ 7 Availability</div></div>
|
||||||
|
<div class="stat"><div class="n" data-count="100">0</div><div class="l">% Cited Answers</div></div>
|
||||||
|
<div class="stat"><div class="n" data-count="3">0</div><div class="l">Knowledge Pillars</div></div>
|
||||||
|
<div class="stat"><div class="n" data-count="1">0</div><div class="l">Bug Squasher</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<section id="features"><div class="wrap">
|
||||||
|
<h2 class="reveal">Everything In One Place</h2>
|
||||||
|
<p class="lead reveal">Stop digging through folders. Ask the brain.</p>
|
||||||
|
<div class="grid">
|
||||||
|
<div class="card tilt reveal"><span class="ico">💬</span><h3>AI Chat</h3><p>Ask anything about your operations. Answers come with citations straight from your documents.</p></div>
|
||||||
|
<div class="card tilt reveal"><span class="ico">📄</span><h3>Document Vault</h3><p>Every ingested file, organized and retrievable. Drop it in, never lose it again.</p></div>
|
||||||
|
<div class="card tilt reveal"><span class="ico">🧩</span><h3>Knowledge Chunks</h3><p>Documents broken into searchable knowledge chunks — the raw material for every answer.</p></div>
|
||||||
|
<div class="card tilt reveal"><span class="ico">🐛</span><h3>Bug Squasher</h3><p>Click the bugs before they escape. The only pest-control game built into a knowledge base.</p></div>
|
||||||
|
</div>
|
||||||
|
</div></section>
|
||||||
|
<section id="knowledge"><div class="wrap">
|
||||||
|
<div class="cta-band reveal">
|
||||||
|
<h2>Stop Searching. Start Knowing.</h2>
|
||||||
|
<p class="lead" style="margin:14px auto 34px">Your entire pest-control knowledge base, one click away.</p>
|
||||||
|
<a class="btn btn-primary" href="/app">🐜 Enter WorkBrain</a>
|
||||||
|
</div>
|
||||||
|
</div></section>
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
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'<a href="{u}">{l}</a>' for l, u in p["nav"])
|
||||||
|
footer = (f'<footer>© 2026 {p["title"].split("—")[0].strip()} · '
|
||||||
|
f'<a href="https://buymeacoffee.com/r26xrthzttg">☕ Buy Me a Coffee</a></footer>')
|
||||||
|
html = f"""<!DOCTYPE html>
|
||||||
|
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<title>{p['title']}</title><meta name="description" content="{p['title']}"><link rel="icon" href="data:,">
|
||||||
|
{css}</head><body>
|
||||||
|
<canvas id="fx"></canvas>
|
||||||
|
<nav><div class="brand"><span class="dot"></span>{p['title'].split('—')[0].strip()}</div>
|
||||||
|
<div class="navlinks">{nav_html}<a class="btn btn-primary" style="padding:10px 22px;font-size:.9rem" href="{p['navcta'][1]}">{p['navcta'][0]}</a></div></nav>
|
||||||
|
{p['body']}
|
||||||
|
<div id="stickycta"><a class="btn btn-primary" href="{p['navcta'][1]}">{p['navcta'][0]} →</a></div>
|
||||||
|
{footer}
|
||||||
|
{js}</body></html>"""
|
||||||
|
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")
|
||||||
99
deploy_landings.py
Normal file
99
deploy_landings.py
Normal file
@@ -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("</div></body></html>\\"\\"\\"", old_start)
|
||||||
|
assert old_end != -1, "landing end marker not found"
|
||||||
|
old_end += len("</div></body></html>\\"\\"\\"")
|
||||||
|
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")
|
||||||
19
fix_nan.py
Normal file
19
fix_nan.py
Normal file
@@ -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")
|
||||||
23
fix_shim_var.py
Normal file
23
fix_shim_var.py
Normal file
@@ -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")
|
||||||
147
full_home_tunnel.py
Normal file
147
full_home_tunnel.py
Normal file
@@ -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]}")
|
||||||
30
home_tunnel.py
Normal file
30
home_tunnel.py
Normal file
@@ -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]}")
|
||||||
32
honest_online.py
Normal file
32
honest_online.py
Normal file
@@ -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")
|
||||||
66
liveness.py
Normal file
66
liveness.py
Normal file
@@ -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")
|
||||||
58
new_fleet_tunnel.py
Normal file
58
new_fleet_tunnel.py
Normal file
@@ -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])
|
||||||
24
patch_satsspin_play.py
Normal file
24
patch_satsspin_play.py
Normal file
@@ -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("<title>My Google AI Studio App</title>", "<title>SatsSpin — Casino</title>")
|
||||||
|
io.open(A, "w", encoding="utf-8").write(app)
|
||||||
|
print("TITLE FIXED")
|
||||||
15
patch_shim.py
Normal file
15
patch_shim.py
Normal file
@@ -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")
|
||||||
18
remove_dup_leaderboard.py
Normal file
18
remove_dup_leaderboard.py
Normal file
@@ -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")
|
||||||
50
rewrite_shim.py
Normal file
50
rewrite_shim.py
Normal file
@@ -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('<script src="/shim.js"></script>', '<script src="/shim.js?v=2"></script>')
|
||||||
|
io.open(A, "w", encoding="utf-8").write(app)
|
||||||
|
print("SHIM REWRITTEN + CACHE-BUSTED")
|
||||||
80
takeover.py
Normal file
80
takeover.py
Normal file
@@ -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")
|
||||||
Reference in New Issue
Block a user