Snapshot: full project state

This commit is contained in:
2026-10-06 23:43:23 -07:00
commit ed212cdbd9
21 changed files with 2090 additions and 0 deletions

17
.gitignore vendored Normal file
View 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

17
README.md Normal file
View File

@@ -0,0 +1,17 @@
# Quasar — Supernova SMM Reseller Panel
Cinematic galaxy-themed social-media-marketing reseller panel. Customers deposit
Bitcoin via BTCPay, buy followers/likes/views, and fulfillment rides the upstream
SMM panel API — the operator keeps the markup.
## Stack
- Flask + SQLite (`supernova.db`), server-rendered Jinja templates
- BTCPay webhook deposits (HMAC-verified)
- `panel_client.py` — upstream SMM API client (order/fulfill/status)
- `deploy.sh` → CT767 (10.30.20.203), nginx + systemd units included
## Layout
- `app.py` — routes: auth, deposit, order, dashboard, admin
- `templates/` — base, auth, dashboard, deposit, order, services, orders
- `supernova.nginx` / `supernova.service` — deploy configs

454
app.py Normal file
View File

@@ -0,0 +1,454 @@
"""Supernova — a cinematic galaxy-themed SMM reseller panel.
Customers deposit Bitcoin (BTCPay) into the operator's wallet, then buy social
media engagement (followers/likes/views) which the operator fulfils through the
upstream SMM panel API. The operator pockets the markup.
"""
import os
import json
import time
import sqlite3
import hashlib
import secrets
import hmac
from functools import wraps
import requests
from flask import (Flask, request, session, redirect, url_for, render_template,
jsonify, abort, flash)
from panel_client import PanelClient
# --------------------------------------------------------------------------
# CONFIG
# --------------------------------------------------------------------------
SMM_API_KEY = os.environ.get("SMM_API_KEY", "7f8a4e57d3568202aa18efe91b48c9c8")
MARKUP_PCT = float(os.environ.get("MARKUP_PCT", "100")) # 100% = 2x wholesale
# BTCPay Server (Greenfield)
BTCPAY_URL = os.environ.get("BTCPAY_URL", "https://10.30.20.140")
BTCPAY_API_KEY = os.environ.get("BTCPAY_API_KEY", "")
BTCPAY_STORE_ID = os.environ.get("BTCPAY_STORE_ID", "")
BTCPAY_WEBHOOK_SECRET = os.environ.get("BTCPAY_WEBHOOK_SECRET", "")
BTCPAY_PUBLIC = os.environ.get("BTCPAY_PUBLIC", "https://btcpay.thetempleofdoom.com")
SITE_URL = os.environ.get("SITE_URL", "https://supernova.thetempleofdoom.com")
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "supernova.db")
SERVICE_CACHE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "services.json")
app = Flask(__name__)
app.secret_key = os.environ.get("SECRET_KEY", secrets.token_hex(32))
panel = PanelClient(SMM_API_KEY)
# --------------------------------------------------------------------------
# DB
# --------------------------------------------------------------------------
def db():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def init_db():
c = db()
c.executescript("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
balance REAL NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
service_id INTEGER NOT NULL,
service_name TEXT NOT NULL,
link TEXT NOT NULL,
quantity INTEGER NOT NULL,
cost REAL NOT NULL,
panel_order_id INTEGER,
status TEXT NOT NULL DEFAULT 'pending',
created_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS deposits (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
amount REAL NOT NULL,
invoice_id TEXT,
status TEXT NOT NULL DEFAULT 'pending',
created_at INTEGER NOT NULL
);
""")
c.commit()
c.close()
# --------------------------------------------------------------------------
# Auth
# --------------------------------------------------------------------------
def hash_password(pw, salt=None):
salt = salt or secrets.token_hex(16)
dk = hashlib.pbkdf2_hmac("sha256", pw.encode(), salt.encode(), 200_000)
return f"{salt}${dk.hex()}"
def verify_password(pw, stored):
salt, _ = stored.split("$", 1)
return hmac.compare_digest(hash_password(pw, salt), stored)
def current_user():
uid = session.get("uid")
if not uid:
return None
c = db()
u = c.execute("SELECT * FROM users WHERE id=?", (uid,)).fetchone()
c.close()
return u
def login_required(f):
@wraps(f)
def wrap(*a, **kw):
if not current_user():
return redirect(url_for("login", next=request.path))
return f(*a, **kw)
return wrap
# --------------------------------------------------------------------------
# Service cache (avoid hitting the panel API every page load)
# --------------------------------------------------------------------------
def get_services(force=False):
if not force and os.path.exists(SERVICE_CACHE):
if time.time() - os.path.getmtime(SERVICE_CACHE) < 900: # 15 min TTL
with open(SERVICE_CACHE) as f:
return json.load(f)
svcs = panel.services()
with open(SERVICE_CACHE, "w") as f:
json.dump(svcs, f)
return svcs
def wholesale_cost(rate, quantity):
return float(rate) * quantity / 1000.0
def retail_price(rate, quantity):
return wholesale_cost(rate, quantity) * (1 + MARKUP_PCT / 100.0)
# --------------------------------------------------------------------------
# BTCPay
# --------------------------------------------------------------------------
def btcpay_headers():
return {"Authorization": f"token {BTCPAY_API_KEY}",
"Content-Type": "application/json"}
def create_invoice(amount_usd, order_id, buyer_email):
# price in USD; BTCPay converts to BTC at its rate
payload = {
"amount": str(round(amount_usd, 2)),
"currency": "USD",
"checkout": {"redirectURL": SITE_URL + "/dashboard"},
"metadata": {"orderId": str(order_id),
"buyerEmail": buyer_email},
}
r = requests.post(f"{BTCPAY_URL}/api/v1/stores/{BTCPAY_STORE_ID}/invoices",
headers=btcpay_headers(), json=payload, timeout=20, verify=False)
r.raise_for_status()
inv = r.json()
# rewrite checkoutLink from LAN IP to public host (customer-facing)
link = inv.get("checkoutLink", "")
if link and link.startswith(BTCPAY_URL):
inv["checkoutLink"] = BTCPAY_PUBLIC + link[len(BTCPAY_URL):]
return inv
def get_invoice(invoice_id):
r = requests.get(f"{BTCPAY_URL}/api/v1/stores/{BTCPAY_STORE_ID}/invoices/{invoice_id}",
headers=btcpay_headers(), timeout=20, verify=False)
r.raise_for_status()
return r.json()
# --------------------------------------------------------------------------
# Routes
# --------------------------------------------------------------------------
@app.route("/")
def index():
return render_template("index.html", user=current_user())
@app.route("/robots.txt")
def robots():
body = ("User-agent: *\n"
"Allow: /\n"
"Sitemap: https://supernova.thetempleofdoom.com/sitemap.xml\n")
return body, 200, {"Content-Type": "text/plain"}
@app.route("/sitemap.xml")
def sitemap():
pages = [
("/", "1.0", "weekly"),
("/services", "0.9", "daily"),
("/signup", "0.5", "monthly"),
("/login", "0.3", "monthly"),
]
urls = "".join(
f"<url><loc>https://supernova.thetempleofdoom.com{p}</loc>"
f"<changefreq>{freq}</changefreq><priority>{pri}</priority></url>"
for p, pri, freq in pages
)
xml = ('<?xml version="1.0" encoding="UTF-8"?>\n'
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'
f"{urls}</urlset>")
return xml, 200, {"Content-Type": "application/xml"}
@app.route("/bg-demo")
def bg_demo():
theme = request.args.get("theme", "cosmos")
themes = ["starfield", "cosmos", "blackhole", "geometric", "nebula", "particles", "waves", "grid"]
return render_template("bg-demo.html", user=current_user(), theme=theme, themes=themes)
@app.route("/signup", methods=["GET", "POST"])
def signup():
if request.method == "POST":
email = request.form.get("email", "").strip().lower()
pw = request.form.get("password", "")
if len(pw) < 8:
flash("Password must be at least 8 characters.", "error")
return redirect(url_for("signup"))
c = db()
try:
c.execute("INSERT INTO users (email, password_hash, created_at) VALUES (?,?,?)",
(email, hash_password(pw), int(time.time())))
c.commit()
except sqlite3.IntegrityError:
c.close()
flash("That email is already registered.", "error")
return redirect(url_for("signup"))
uid = c.execute("SELECT id FROM users WHERE email=?", (email,)).fetchone()["id"]
c.close()
session["uid"] = uid
return redirect(url_for("dashboard"))
return render_template("auth.html", mode="signup", user=None)
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
email = request.form.get("email", "").strip().lower()
pw = request.form.get("password", "")
c = db()
u = c.execute("SELECT * FROM users WHERE email=?", (email,)).fetchone()
c.close()
if u and verify_password(pw, u["password_hash"]):
session["uid"] = u["id"]
nxt = request.args.get("next") or url_for("dashboard")
return redirect(nxt)
flash("Invalid email or password.", "error")
return render_template("auth.html", mode="login", user=None)
@app.route("/logout")
def logout():
session.clear()
return redirect(url_for("index"))
@app.route("/services")
def services():
svcs = get_services()
# group by category
cats = {}
for s in svcs:
cats.setdefault(s["category"], []).append(s)
return render_template("services.html", user=current_user(), cats=cats)
@app.route("/order/<int:service_id>", methods=["GET", "POST"])
@login_required
def order(service_id):
svcs = {s["service"]: s for s in get_services()}
s = svcs.get(service_id)
if not s:
abort(404)
if request.method == "POST":
link = request.form.get("link", "").strip()
quantity = int(request.form.get("quantity", 0))
cost = retail_price(s["rate"], quantity)
if not link or quantity < s["min"]:
flash(f"Enter a valid link and quantity (min {s['min']}).", "error")
return redirect(url_for("order", service_id=service_id))
u = current_user()
if u["balance"] < cost:
flash("Insufficient balance — deposit more BTC first.", "error")
return redirect(url_for("deposit"))
# charge + place upstream order
try:
res = panel.place_order(service_id, link, quantity)
panel_oid = res.get("order")
except Exception as e:
flash(f"Upstream order failed: {e}", "error")
return redirect(url_for("order", service_id=service_id))
c = db()
c.execute("UPDATE users SET balance = balance - ? WHERE id=?",
(cost, u["id"]))
c.execute("""INSERT INTO orders
(user_id, service_id, service_name, link, quantity, cost,
panel_order_id, status, created_at)
VALUES (?,?,?,?,?,?,?,?,?)""",
(u["id"], service_id, s["name"], link, quantity, cost,
panel_oid, "pending", int(time.time())))
c.commit()
c.close()
flash(f"Order placed! ${cost:.2f} — it'll start delivering shortly.", "success")
return redirect(url_for("orders") + "?launched=1")
return render_template("order.html", user=current_user(), s=s,
markup=MARKUP_PCT)
@app.route("/orders")
@login_required
def orders():
u = current_user()
c = db()
rows = c.execute("SELECT * FROM orders WHERE user_id=? ORDER BY id DESC LIMIT 100",
(u["id"],)).fetchall()
c.close()
return render_template("orders.html", user=u, orders=rows)
@app.route("/dashboard")
@login_required
def dashboard():
u = current_user()
c = db()
orders = c.execute("SELECT * FROM orders WHERE user_id=? ORDER BY id DESC LIMIT 10",
(u["id"],)).fetchall()
deposits = c.execute("SELECT * FROM deposits WHERE user_id=? ORDER BY id DESC LIMIT 10",
(u["id"],)).fetchall()
c.close()
return render_template("dashboard.html", user=u, orders=orders, deposits=deposits)
@app.route("/deposit", methods=["GET", "POST"])
@login_required
def deposit():
u = current_user()
if request.method == "POST":
amount = float(request.form.get("amount", 0))
if amount <= 0:
flash("Enter a valid amount.", "error")
return redirect(url_for("deposit"))
c = db()
cur = c.execute("""INSERT INTO deposits (user_id, amount, status, created_at)
VALUES (?,?,'pending',?)""",
(u["id"], amount, int(time.time())))
c.commit()
dep_id = cur.lastrowid
c.close()
inv = create_invoice(amount, dep_id, u["email"])
c = db()
c.execute("UPDATE deposits SET invoice_id=? WHERE id=?",
(inv["id"], dep_id))
c.commit()
c.close()
return redirect(inv["checkoutLink"])
return render_template("deposit.html", user=u)
@app.route("/webhook/btcpay", methods=["POST"])
def btcpay_webhook():
# verify signature
sig = request.headers.get("BTCPay-Sig", "")
body = request.get_data()
expected = "sha256=" + hmac.new(BTCPAY_WEBHOOK_SECRET.encode(), body,
hashlib.sha256).hexdigest()
if BTCPAY_WEBHOOK_SECRET and not hmac.compare_digest(sig, expected):
return "bad signature", 401
data = request.get_json(silent=True) or {}
inv_id = data.get("invoiceId")
if not inv_id:
return "ok", 200
# re-fetch invoice to confirm status (don't trust the webhook blindly)
try:
inv = get_invoice(inv_id)
except Exception:
return "ok", 200
if inv.get("status") != "Settled":
return "ok", 200
dep_id = (inv.get("metadata") or {}).get("orderId")
amount = float(inv.get("amount", 0))
c = db()
dep = c.execute("SELECT * FROM deposits WHERE id=?", (dep_id,)).fetchone()
if dep and dep["status"] != "credited":
c.execute("UPDATE deposits SET status='credited' WHERE id=?", (dep_id,))
c.execute("UPDATE users SET balance = balance + ? WHERE id=?",
(amount, dep["user_id"]))
c.commit()
c.close()
return "ok", 200
@app.route("/api/quote/<int:service_id>/<int:quantity>")
def api_quote(service_id, quantity):
svcs = {s["service"]: s for s in get_services()}
s = svcs.get(service_id)
if not s:
return jsonify({"error": "unknown service"}), 404
return jsonify({
"quantity": quantity,
"wholesale": round(wholesale_cost(s["rate"], quantity), 4),
"retail": round(retail_price(s["rate"], quantity), 2),
"rate": s["rate"],
"min": s["min"],
"max": s["max"],
})
@app.route("/api/orders-status")
@login_required
def api_orders_status():
"""Live delivery status for the dashboard ticker."""
u = current_user()
c = db()
rows = c.execute("SELECT * FROM orders WHERE user_id=? ORDER BY id DESC LIMIT 8",
(u["id"],)).fetchall()
c.close()
out = []
for o in rows:
status = o["status"]
remains = 0
delivered = o["quantity"]
if o["panel_order_id"]:
try:
st = panel.order_status(o["panel_order_id"])
status = (st.get("status") or o["status"]).lower().replace(" ", "")
remains = int(st.get("remains") or 0)
delivered = max(0, o["quantity"] - remains)
except Exception:
pass
out.append({
"id": o["id"],
"name": o["service_name"],
"quantity": o["quantity"],
"delivered": delivered,
"status": status,
})
return jsonify({"orders": out})
if __name__ == "__main__":
init_db()
app.run(host="0.0.0.0", port=5000)

2
credit.sh Normal file
View File

@@ -0,0 +1,2 @@
#!/bin/bash
sqlite3 /opt/supernova/supernova.db "UPDATE users SET balance=10.00 WHERE email='test@example.com'; UPDATE deposits SET status='credited' WHERE id=2; SELECT email,balance FROM users WHERE email='test@example.com';"

38
deploy.sh Normal file
View File

@@ -0,0 +1,38 @@
#!/bin/bash
# Deploy Supernova to CT767 (10.30.20.203)
set -e
export SSHPASS='Czapiewski1!'
SSH="/opt/homebrew/bin/sshpass -e ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=20 root@10.30.20.85"
SCP="/opt/homebrew/bin/sshpass -e scp -O -o StrictHostKeyChecking=accept-new"
CT="pct exec 767 --"
cd /Users/drjones/quasar-app
echo "=== packaging app ==="
tar czf /tmp/supernova-app.tar.gz app.py panel_client.py templates static
echo "=== pushing to CT767 ==="
$SCP /tmp/supernova-app.tar.gz root@10.30.20.85:/tmp/supernova-app.tar.gz
$SCP .env supernova.service supernova.nginx root@10.30.20.85:/tmp/
echo "=== installing on CT ==="
$SSH "
$CT bash -c '
mkdir -p /opt/supernova
tar xzf /tmp/supernova-app.tar.gz -C /opt/supernova
cp /tmp/.env /opt/supernova/.env
cp /tmp/supernova.service /etc/systemd/system/supernova.service
cp /tmp/supernova.nginx /etc/nginx/sites-available/supernova
ln -sf /etc/nginx/sites-available/supernova /etc/nginx/sites-enabled/supernova
rm -f /etc/nginx/sites-enabled/default
systemctl daemon-reload
systemctl enable supernova
systemctl restart supernova
nginx -t && systemctl reload nginx
'
"
echo "=== verifying ==="
sleep 3
$SSH "$CT bash -c 'systemctl is-active supernova; curl -s -m 5 http://127.0.0.1:5000/ -o /dev/null -w \"app :5000 -> %{http_code}\n\"; curl -s -m 5 http://127.0.0.1:80/ -o /dev/null -w \"nginx :80 -> %{http_code}\n\"'"
echo "=== DONE ==="

16
link-wallet.sh Normal file
View File

@@ -0,0 +1,16 @@
#!/bin/bash
PW=$(grep -oE "Password=[^;]*" /etc/btcpay/btcpay.config | head -1 | cut -d= -f2)
export PGPASSWORD="$PW"
PSQL="psql -h localhost -U btcpay -d btcpay -t -A"
SRC=$($PSQL -c "SELECT \"StoreName\" FROM \"Stores\" WHERE \"DerivationStrategies\" IS NOT NULL AND \"DerivationStrategies\"::text != '{}' ORDER BY \"StoreName\" LIMIT 1")
echo "source store: $SRC"
$PSQL -c "UPDATE \"Stores\" SET \"DerivationStrategies\" = (SELECT \"DerivationStrategies\" FROM \"Stores\" WHERE \"StoreName\"='$SRC'), \"DerivationStrategy\"='BTC-CHAIN' WHERE \"StoreName\"='Supernova'"
echo "wallet copy done"
$PSQL -c "UPDATE \"Stores\" SET \"StoreBlob\" = jsonb_set(COALESCE(\"StoreBlob\",'{}'), '{networkFeeMode}', '\"Always\"', true) WHERE \"StoreName\"='Supernova'"
echo "networkFeeMode set"
echo "=== verify ==="
$PSQL -c "SELECT \"StoreName\", \"DerivationStrategy\", (\"DerivationStrategies\"->'BTC-CHAIN'->>'accountDerivation') IS NOT NULL AS has_xpub FROM \"Stores\" WHERE \"StoreName\"='Supernova'"

32
panel_client.py Normal file
View File

@@ -0,0 +1,32 @@
"""Supernova — SMM reseller panel client.
Wraps the upstream SMM panel API (PerfectPanel-style v2).
Rate is per-1000 units.
"""
import requests
BASE = "https://therealblackmarket.net/api/v2"
class PanelClient:
def __init__(self, api_key):
self.key = api_key
def _call(self, action, **params):
data = {"key": self.key, "action": action}
data.update(params)
r = requests.post(BASE, data=data, timeout=25)
r.raise_for_status()
return r.json()
def balance(self):
return self._call("balance")
def services(self):
return self._call("services")
def place_order(self, service_id, link, quantity):
return self._call("add", service=service_id, link=link, quantity=quantity)
def order_status(self, order_id):
return self._call("status", order=order_id)

315
static/app.js Normal file
View File

@@ -0,0 +1,315 @@
// Supernova — slow, cinematic starfield + satisfying interactivity.
// ---------------------------------------------------------------
// Starfield (slow, gentle parallax drift)
// ---------------------------------------------------------------
(function () {
const canvas = document.getElementById('starfield');
if (!canvas) return;
const ctx = canvas.getContext('2d');
let stars = [], W, H;
function resize() { W = canvas.width = innerWidth; H = canvas.height = innerHeight; }
function makeStars() {
stars = [];
const n = Math.min(260, Math.floor(W * H / 6000));
for (let i = 0; i < n; i++) {
stars.push({
x: Math.random() * W, y: Math.random() * H,
r: Math.random() * 1.3 + 0.3,
tw: Math.random() * Math.PI * 2, twSpeed: 0.4 + Math.random() * 0.9,
drift: 0.04 + Math.random() * 0.18, hue: Math.random(),
});
}
}
let t = 0;
function frame() {
t += 0.016;
ctx.clearRect(0, 0, W, H);
for (const s of stars) {
s.y += s.drift; s.x += s.drift * 0.15;
if (s.y > H + 4) { s.y = -4; s.x = Math.random() * W; }
if (s.x > W + 4) s.x = -4;
const tw = 0.55 + 0.45 * Math.sin(t * s.twSpeed + s.tw);
const a = tw * (0.5 + 0.5 * s.r / 1.6);
let col = s.hue < 0.72 ? `rgba(207,224,255,${a})`
: s.hue < 0.86 ? `rgba(186,160,255,${a})`
: s.hue < 0.95 ? `rgba(160,200,255,${a})` : `rgba(255,180,220,${a})`;
ctx.beginPath(); ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
ctx.fillStyle = col; ctx.fill();
}
requestAnimationFrame(frame);
}
function shootingStar() {
const x = Math.random() * W * 0.7 + W * 0.15, y = Math.random() * H * 0.4;
const len = 90 + Math.random() * 60, ang = Math.PI * 0.75; let p = 0;
const step = () => {
p += 0.04;
const cx = x + Math.cos(ang) * len * p, cy = y + Math.sin(ang) * len * p;
const g = ctx.createLinearGradient(cx, cy, cx - Math.cos(ang) * len * 0.4, cy - Math.sin(ang) * len * 0.4);
g.addColorStop(0, 'rgba(255,255,255,0.85)'); g.addColorStop(1, 'rgba(255,255,255,0)');
ctx.strokeStyle = g; ctx.lineWidth = 1.6;
ctx.beginPath(); ctx.moveTo(cx, cy);
ctx.lineTo(cx - Math.cos(ang) * len * 0.4, cy - Math.sin(ang) * len * 0.4); ctx.stroke();
if (p < 1) requestAnimationFrame(step);
};
step(); setTimeout(shootingStar, 9000 + Math.random() * 12000);
}
resize(); makeStars(); requestAnimationFrame(frame);
setTimeout(shootingStar, 4000);
addEventListener('resize', () => { resize(); makeStars(); });
})();
// ---------------------------------------------------------------
// Confetti celebration (gentle galaxy burst)
// ---------------------------------------------------------------
function celebrate() {
const canvas = document.createElement('canvas');
canvas.style.cssText = 'position:fixed;inset:0;pointer-events:none;z-index:9999';
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
canvas.width = innerWidth; canvas.height = innerHeight;
const colors = ['#7b2ff7', '#2b6cff', '#ff2d95', '#ffffff', '#cfe0ff', '#7cffb0'];
const parts = [];
for (let i = 0; i < 150; i++) {
parts.push({
x: Math.random() * canvas.width,
y: -20 - Math.random() * canvas.height * 0.4,
r: Math.random() * 4 + 2,
vy: 1.4 + Math.random() * 2.2,
vx: (Math.random() - 0.5) * 1.6,
rot: Math.random() * Math.PI * 2, vr: (Math.random() - 0.5) * 0.07,
color: colors[Math.floor(Math.random() * colors.length)],
star: Math.random() < 0.45,
});
}
let frames = 0;
function starPath(c, r) {
c.beginPath();
for (let i = 0; i < 5; i++) {
const a = (i * 4 * Math.PI) / 5 - Math.PI / 2;
const a2 = a + (2 * Math.PI) / 5;
c.lineTo(Math.cos(a) * r, Math.sin(a) * r);
c.lineTo(Math.cos((a + a2) / 2) * r * 0.4, Math.sin((a + a2) / 2) * r * 0.4);
}
c.closePath(); c.fill();
}
function step() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
frames++;
for (const p of parts) {
p.x += p.vx; p.y += p.vy; p.rot += p.vr; p.vy += 0.015;
ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(p.rot);
ctx.globalAlpha = Math.max(0, 1 - frames / 250);
ctx.fillStyle = p.color;
if (p.star) starPath(ctx, p.r);
else { ctx.beginPath(); ctx.arc(0, 0, p.r, 0, Math.PI * 2); ctx.fill(); }
ctx.restore();
}
if (frames < 270 && parts.some(p => p.y < canvas.height + 40)) requestAnimationFrame(step);
else canvas.remove();
}
requestAnimationFrame(step);
}
// ---------------------------------------------------------------
// Subtle 3D card tilt (slow, max 4deg)
// ---------------------------------------------------------------
(function () {
const cards = document.querySelectorAll('.card');
cards.forEach(card => {
card.addEventListener('mousemove', e => {
const r = card.getBoundingClientRect();
const px = (e.clientX - r.left) / r.width - 0.5;
const py = (e.clientY - r.top) / r.height - 0.5;
card.style.transform = `perspective(900px) rotateX(${(-py * 4).toFixed(2)}deg) rotateY(${(px * 4).toFixed(2)}deg) translateY(-3px)`;
});
card.addEventListener('mouseleave', () => { card.style.transform = ''; });
});
})();
// ---------------------------------------------------------------
// Scroll reveal (fade + rise, gentle)
// ---------------------------------------------------------------
(function () {
const els = document.querySelectorAll('.card, .stat, .seo-block, .section-title');
if (!('IntersectionObserver' in window)) return;
const io = new IntersectionObserver(entries => {
entries.forEach(en => {
if (en.isIntersecting) { en.target.classList.add('revealed'); io.unobserve(en.target); }
});
}, { threshold: 0.08 });
els.forEach(el => {
el.classList.add('reveal');
io.observe(el);
});
})();
// ---------------------------------------------------------------
// Button ripple
// ---------------------------------------------------------------
(function () {
document.addEventListener('click', e => {
const btn = e.target.closest('.btn');
if (!btn) return;
const r = btn.getBoundingClientRect();
const rip = document.createElement('span');
const size = Math.max(r.width, r.height) * 1.6;
rip.style.cssText = `position:absolute;border-radius:50%;width:${size}px;height:${size}px;` +
`left:${e.clientX - r.left - size / 2}px;top:${e.clientY - r.top - size / 2}px;` +
`background:radial-gradient(circle,rgba(255,255,255,0.45),rgba(255,255,255,0));` +
`transform:scale(0);animation:ripple .6s ease-out forwards;pointer-events:none`;
btn.style.position = 'relative'; btn.style.overflow = 'hidden';
btn.appendChild(rip);
setTimeout(() => rip.remove(), 650);
});
})();
// ---------------------------------------------------------------
// Quantity slider — animated price counter + value tween
// ---------------------------------------------------------------
(function () {
const slider = document.getElementById('qty');
const priceEl = document.getElementById('live-price');
const qtyEl = document.getElementById('qty-val');
if (!slider || !priceEl) return;
const serviceId = parseInt(slider.dataset.service, 10);
let shown = 0;
function render(target, qty) {
const start = performance.now(), from = shown, dur = 320;
function step(t) {
const p = Math.min(1, (t - start) / dur);
const eased = 1 - Math.pow(1 - p, 3);
const val = from + (target - from) * eased;
priceEl.innerHTML = `$${val.toFixed(2)} <small>for ${qty.toLocaleString()} ×</small>`;
if (p < 1) requestAnimationFrame(step); else shown = target;
}
requestAnimationFrame(step);
if (qtyEl) qtyEl.textContent = qty.toLocaleString();
}
function update() {
const q = parseInt(slider.value, 10);
fetch(`/api/quote/${serviceId}/${q}`)
.then(r => r.json()).then(d => render(d.retail, q))
.catch(() => {});
}
slider.addEventListener('input', update);
update();
})();
// ---------------------------------------------------------------
// Order success celebration (detect ?launched=1)
// ---------------------------------------------------------------
(function () {
if (new URLSearchParams(location.search).has('launched')) {
const banner = document.getElementById('launch-banner');
if (banner) {
banner.classList.add('show');
setTimeout(() => banner.classList.remove('show'), 5000);
}
celebrate();
}
})();
// ---------------------------------------------------------------
// Launch whoosh (Web Audio synth — subtle rising sweep)
// ---------------------------------------------------------------
function launchSound() {
try {
const AC = window.AudioContext || window.webkitAudioContext;
if (!AC) return;
const ctx = new AC();
const dur = 0.55;
const buf = ctx.createBuffer(1, ctx.sampleRate * dur, ctx.sampleRate);
const d = buf.getChannelData(0);
for (let i = 0; i < d.length; i++) {
const t = i / d.length;
d[i] = (Math.random() * 2 - 1) * Math.pow(t, 1.6) * (1 - t) * 0.9;
}
const src = ctx.createBufferSource(); src.buffer = buf;
const filt = ctx.createBiquadFilter();
filt.type = 'bandpass'; filt.Q.value = 1.1;
filt.frequency.setValueAtTime(280, ctx.currentTime);
filt.frequency.exponentialRampToValueAtTime(2600, ctx.currentTime + dur);
const gain = ctx.createGain();
gain.gain.setValueAtTime(0.0001, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.22, ctx.currentTime + dur * 0.28);
gain.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + dur);
src.connect(filt).connect(gain).connect(ctx.destination);
src.start();
setTimeout(() => ctx.close(), dur * 1000 + 200);
} catch (e) {}
}
(function () {
const btn = document.querySelector('.btn.launch');
if (btn) btn.addEventListener('click', launchSound);
})();
// ---------------------------------------------------------------
// Orbital planet picker — drag to spin, slow auto-rotate
// ---------------------------------------------------------------
(function () {
const ring = document.getElementById('orbit');
if (!ring) return;
const planets = Array.from(ring.querySelectorAll('.planet-orb'));
let angle = 0, vel = 0.06, dragging = false, lastX = 0;
function layout() {
const R = 175;
planets.forEach(p => {
const i = parseFloat(p.dataset.i);
const a = ((i * 45 + angle) * Math.PI) / 180;
p.style.transform = `translate(${(Math.cos(a) * R).toFixed(1)}px, ${(Math.sin(a) * R).toFixed(1)}px)`;
});
}
function tick() {
if (!dragging) { vel += (0.06 - vel) * 0.03; angle += vel; }
layout();
requestAnimationFrame(tick);
}
const down = x => { dragging = true; lastX = x; ring.style.cursor = 'grabbing'; };
const move = x => { if (!dragging) return; vel = (x - lastX) * 0.18; angle += vel; lastX = x; layout(); };
const up = () => { dragging = false; ring.style.cursor = 'grab'; };
ring.style.cursor = 'grab';
ring.addEventListener('mousedown', e => down(e.clientX));
window.addEventListener('mousemove', e => move(e.clientX));
window.addEventListener('mouseup', up);
ring.addEventListener('touchstart', e => down(e.touches[0].clientX), { passive: true });
window.addEventListener('touchmove', e => move(e.touches[0].clientX), { passive: true });
window.addEventListener('touchend', up);
requestAnimationFrame(tick);
})();
// ---------------------------------------------------------------
// Delivery ticker (dashboard live progress, poll every 45s)
// ---------------------------------------------------------------
(function () {
const wrap = document.getElementById('ticker');
if (!wrap) return;
const fmt = n => n.toLocaleString();
function bar(name, delivered, total, status) {
const pct = total > 0 ? Math.min(100, (delivered / total) * 100) : 0;
const label = status === 'completed' ? '✅ Done' : status === 'canceled' ? 'Cancelled'
: status === 'inprogress' ? '⚡ Delivering…' : '⏳ Queued';
return `<div class="tick">
<div class="tick-top"><span class="tick-name">${name}</span><span class="tick-meta">${fmt(delivered)} / ${fmt(total)} · ${label}</span></div>
<div class="tick-bar"><div class="tick-fill" style="width:${pct}%"></div></div>
</div>`;
}
function poll() {
fetch('/api/orders-status').then(r => r.json()).then(d => {
if (!d.orders || !d.orders.length) {
wrap.innerHTML = '<div class="muted center" style="padding:18px">No boosts yet — <a href="/services" style="color:var(--nebula-b)">launch one →</a></div>';
return;
}
wrap.innerHTML = d.orders.map(o => bar(o.name, o.delivered, o.quantity, o.status)).join('');
}).catch(() => {});
}
poll();
setInterval(poll, 45000);
})();

303
static/cosmic-bg.js Normal file
View File

@@ -0,0 +1,303 @@
/* cosmic-bg.js — self-contained animated backgrounds for the fleet.
* Drop in with: <script src="cosmic-bg.js" data-theme="blackhole"></script>
* Themes: starfield, cosmos, blackhole, geometric, nebula, particles, waves, grid
* Config (data attrs): data-theme, data-density (low/med/high), data-speed (0.5-2)
* All motion is slow + smooth by design. Visibility tuned to be clearly visible.
*/
(function () {
"use strict";
var s = document.currentScript;
var theme = (s && s.dataset.theme) || "cosmos";
var density = (s && s.dataset.density) || "med";
var speed = parseFloat((s && s.dataset.speed) || "1");
var D = density === "low" ? 0.5 : density === "high" ? 1.7 : 1;
var c = document.createElement("canvas");
c.style.cssText = "position:fixed;inset:0;z-index:0;pointer-events:none;display:block";
function mount() {
if (c.parentNode) return;
(document.body || document.documentElement).appendChild(c);
var st = document.createElement("style");
st.textContent = "body > *:not(canvas){position:relative;z-index:1}";
(document.head || document.documentElement).appendChild(st);
}
if (document.body) mount();
else document.addEventListener("DOMContentLoaded", mount);
var ctx = c.getContext("2d");
var W, H, DPR = Math.min(window.devicePixelRatio || 1, 2);
function resize() {
W = window.innerWidth; H = window.innerHeight;
c.width = W * DPR; c.height = H * DPR;
c.style.width = W + "px"; c.style.height = H + "px";
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
}
resize();
window.addEventListener("resize", resize);
function rnd(a, b) { return a + Math.random() * (b - a); }
function pick(arr) { return arr[Math.floor(Math.random() * arr.length)]; }
var PALETTES = {
cosmic: ["#a06bff", "#5b8cff", "#ff5ab5", "#e8f0ff"],
warm: ["#ff8a8a", "#ffd93d", "#ffb06a", "#fff"],
cool: ["#4fd8ff", "#6fa8ff", "#bff3f2", "#fff"],
green: ["#9fffd0", "#4de68a", "#c6ff7a", "#fff"],
};
function hexA(hex, a) {
var r = parseInt(hex.slice(1, 3), 16), g = parseInt(hex.slice(3, 5), 16), b = parseInt(hex.slice(5, 7), 16);
return "rgba(" + r + "," + g + "," + b + "," + Math.max(0, Math.min(1, a)).toFixed(3) + ")";
}
function starField(pal, drift) {
var stars = [];
var n = Math.floor(240 * D);
for (var i = 0; i < n; i++) stars.push({
x: Math.random() * W, y: Math.random() * H,
r: rnd(0.4, 2.1), tw: rnd(0, 6.28), tws: rnd(0.3, 1.2),
dr: drift * rnd(0.3, 1), c: pick(pal)
});
return function (dt, t) {
for (var j = 0; j < stars.length; j++) {
var st = stars[j];
st.y += st.dr; st.x += st.dr * 0.12;
if (st.y > H + 4) { st.y = -4; st.x = Math.random() * W; }
var a = (0.6 + 0.4 * Math.sin(t * st.tws + st.tw)) * (0.5 + 0.5 * st.r / 2.1);
ctx.globalAlpha = a; ctx.fillStyle = st.c;
ctx.beginPath(); ctx.arc(st.x, st.y, st.r, 0, 6.28); ctx.fill();
}
ctx.globalAlpha = 1;
};
}
function nebulaBlobs(count, pal) {
var blobs = [];
for (var i = 0; i < count; i++) blobs.push({
x: Math.random() * W, y: Math.random() * H,
r: rnd(140, 360), dx: rnd(-0.3, 0.3), dy: rnd(-0.2, 0.2),
c: pick(pal), ph: rnd(0, 6.28)
});
return function (dt, t) {
for (var j = 0; j < blobs.length; j++) {
var b = blobs[j];
b.x += b.dx; b.y += b.dy;
if (b.x < -b.r) b.x = W + b.r; if (b.x > W + b.r) b.x = -b.r;
if (b.y < -b.r) b.y = H + b.r; if (b.y > H + b.r) b.y = -b.r;
var g = ctx.createRadialGradient(b.x, b.y, 0, b.x, b.y, b.r);
var pulse = 0.16 + 0.06 * Math.sin(t * 0.5 + b.ph);
g.addColorStop(0, hexA(b.c, pulse));
g.addColorStop(1, hexA(b.c, 0));
ctx.fillStyle = g;
ctx.fillRect(b.x - b.r, b.y - b.r, b.r * 2, b.r * 2);
}
};
}
var themes = {};
themes.starfield = function () {
var draw = starField(PALETTES.cosmic, 0.45);
return function (dt, t) { draw(dt, t); };
};
themes.cosmos = function () {
var stars = starField(PALETTES.cosmic, 0.35);
var neb = nebulaBlobs(Math.floor(4 * D), PALETTES.cosmic);
var arms = 3, particles = [];
for (var i = 0; i < Math.floor(360 * D); i++) {
var arm = Math.floor(Math.random() * arms);
var rr = Math.pow(Math.random(), 0.6) * Math.min(W, H) * 0.44;
var aa = (arm / arms) * 6.28 + rr * 0.02 + rnd(-0.2, 0.2);
particles.push({ rr: rr, aa: aa, r: rnd(0.5, 1.8), c: pick(PALETTES.cosmic) });
}
return function (dt, t) {
neb(dt, t);
var cx = W * 0.5, cy = H * 0.5;
for (var j = 0; j < particles.length; j++) {
var p = particles[j];
p.aa += 0.0016 * speed;
var x = cx + Math.cos(p.aa) * p.rr, y = cy + Math.sin(p.aa) * p.rr;
ctx.globalAlpha = 0.75; ctx.fillStyle = p.c;
ctx.beginPath(); ctx.arc(x, y, p.r, 0, 6.28); ctx.fill();
}
ctx.globalAlpha = 1;
stars(dt, t);
};
};
themes.blackhole = function () {
var cx = W * 0.5, cy = H * 0.5;
var bhR = Math.min(W, H) * 0.14;
var acc = [];
for (var i = 0; i < Math.floor(560 * D); i++) {
var ang = rnd(0, 6.28), rad = rnd(bhR * 1.15, Math.min(W, H) * 0.5);
acc.push({ ang: ang, rad: rad, speed: 0.006 + 0.016 * (bhR / rad), r: rnd(0.6, 2.2),
c: pick(["#ffc06a", "#ff8a6a", "#ffd93d", "#ff5ab5", "#fff"]) });
}
return function (dt, t) {
// broad accretion glow
var glow = ctx.createRadialGradient(cx, cy, bhR * 0.7, cx, cy, bhR * 3.6);
glow.addColorStop(0, "rgba(255,150,60,0.5)");
glow.addColorStop(0.45, "rgba(255,45,149,0.22)");
glow.addColorStop(1, "rgba(0,0,0,0)");
ctx.fillStyle = glow; ctx.fillRect(0, 0, W, H);
// particles spiralling in
for (var j = 0; j < acc.length; j++) {
var p = acc[j];
p.ang += p.speed * speed; p.rad -= 0.05 * speed;
if (p.rad < bhR * 1.02) { p.rad = rnd(bhR * 1.5, Math.min(W, H) * 0.5); p.ang = rnd(0, 6.28); }
var x = cx + Math.cos(p.ang) * p.rad, y = cy + Math.sin(p.ang) * p.rad;
var stretch = Math.min(1, (p.rad - bhR) / (bhR * 1.6));
ctx.globalAlpha = 0.5 + 0.5 * stretch;
ctx.fillStyle = p.c;
ctx.beginPath(); ctx.arc(x, y, p.r * (0.5 + stretch), 0, 6.28); ctx.fill();
}
ctx.globalAlpha = 1;
// event horizon + bright photon ring
ctx.fillStyle = "#000"; ctx.beginPath(); ctx.arc(cx, cy, bhR, 0, 6.28); ctx.fill();
var ring = ctx.createRadialGradient(cx, cy, bhR, cx, cy, bhR + 10);
ring.addColorStop(0, "rgba(255,220,150,0.95)"); ring.addColorStop(1, "rgba(255,220,150,0)");
ctx.strokeStyle = "rgba(255,220,150,0.95)"; ctx.lineWidth = 2.5;
ctx.beginPath(); ctx.arc(cx, cy, bhR + 1, 0, 6.28); ctx.stroke();
};
};
themes.geometric = function () {
var polys = [];
var n = Math.floor(22 * D);
for (var i = 0; i < n; i++) polys.push({
x: Math.random() * W, y: Math.random() * H,
sides: pick([3, 4, 6]), size: rnd(22, 60),
rot: rnd(0, 6.28), vr: rnd(-0.0025, 0.0025),
dx: rnd(-0.28, 0.28), dy: rnd(-0.22, 0.22),
c: pick(PALETTES.cosmic)
});
function poly(x, y, r, sides, rot) {
ctx.beginPath();
for (var k = 0; k < sides; k++) {
var a = rot + (k / sides) * 6.28;
var px = x + Math.cos(a) * r, py = y + Math.sin(a) * r;
k === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py);
}
ctx.closePath();
}
return function (dt, t) {
for (var j = 0; j < polys.length; j++) {
var p = polys[j];
p.x += p.dx; p.y += p.dy; p.rot += p.vr;
if (p.x < -70) p.x = W + 70; if (p.x > W + 70) p.x = -70;
if (p.y < -70) p.y = H + 70; if (p.y > H + 70) p.y = -70;
ctx.strokeStyle = hexA(p.c, 0.65); ctx.lineWidth = 1.4;
poly(p.x, p.y, p.size, p.sides, p.rot); ctx.stroke();
ctx.fillStyle = hexA(p.c, 0.12); ctx.fill();
}
for (var a = 0; a < polys.length; a++) {
for (var b = a + 1; b < polys.length; b++) {
var dx = polys[a].x - polys[b].x, dy = polys[a].y - polys[b].y;
var d = Math.sqrt(dx * dx + dy * dy);
if (d < 170) {
ctx.strokeStyle = "rgba(190,200,255," + (0.22 * (1 - d / 170)).toFixed(3) + ")";
ctx.lineWidth = 0.8;
ctx.beginPath(); ctx.moveTo(polys[a].x, polys[a].y); ctx.lineTo(polys[b].x, polys[b].y); ctx.stroke();
}
}
}
};
};
themes.nebula = function () {
var neb = nebulaBlobs(Math.floor(9 * D), PALETTES.cosmic.concat(PALETTES.cool));
var stars = starField(PALETTES.cosmic, 0.3);
return function (dt, t) { neb(dt, t); stars(dt, t); };
};
themes.particles = function () {
var ps = [];
var n = Math.floor(130 * D);
for (var i = 0; i < n; i++) ps.push({
x: Math.random() * W, y: Math.random() * H,
vx: rnd(-0.35, 0.35), vy: rnd(-0.35, 0.35), r: rnd(1.2, 3), c: pick(PALETTES.cosmic)
});
return function (dt, t) {
for (var j = 0; j < ps.length; j++) {
var p = ps[j];
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;
ctx.fillStyle = p.c; ctx.globalAlpha = 0.95;
ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, 6.28); ctx.fill();
}
ctx.globalAlpha = 1;
for (var a = 0; a < ps.length; a++) {
for (var b = a + 1; b < ps.length; b++) {
var dx = ps[a].x - ps[b].x, dy = ps[a].y - ps[b].y;
var d = Math.sqrt(dx * dx + dy * dy);
if (d < 150) {
ctx.strokeStyle = "rgba(190,200,255," + (0.25 * (1 - d / 150)).toFixed(3) + ")";
ctx.lineWidth = 0.8;
ctx.beginPath(); ctx.moveTo(ps[a].x, ps[a].y); ctx.lineTo(ps[b].x, ps[b].y); ctx.stroke();
}
}
}
};
};
themes.waves = function () {
var layers = [
{ amp: 46, len: 260, speed: 0.010, y: 0.65, c: "rgba(160,80,255,0.30)" },
{ amp: 60, len: 320, speed: 0.013, y: 0.72, c: "rgba(70,120,255,0.26)" },
{ amp: 76, len: 400, speed: 0.016, y: 0.80, c: "rgba(255,70,170,0.22)" },
];
return function (dt, t) {
for (var j = 0; j < layers.length; j++) {
var L = layers[j];
ctx.fillStyle = L.c; ctx.beginPath(); ctx.moveTo(0, H);
for (var x = 0; x <= W; x += 8) {
var y = H * L.y + Math.sin(x / L.len + t * L.speed * speed) * L.amp;
ctx.lineTo(x, y);
}
ctx.lineTo(W, H); ctx.closePath(); ctx.fill();
}
};
};
themes.grid = function () {
var horizon = H * 0.55;
return function (dt, t) {
var off = (t * 30 * speed) % 40;
ctx.strokeStyle = "rgba(150,80,255,0.45)"; ctx.lineWidth = 1.2;
for (var i = -12; i <= 12; i++) {
var x0 = W / 2 + i * 55;
ctx.beginPath(); ctx.moveTo(W / 2, horizon); ctx.lineTo(x0, H); ctx.stroke();
}
for (var j = 0; j < 16; j++) {
var yy = horizon + Math.pow((j + off / 40) % 16 / 16, 2) * (H - horizon);
ctx.strokeStyle = "rgba(255,70,170," + (0.10 + 0.35 * ((yy - horizon) / (H - horizon))).toFixed(3) + ")";
ctx.beginPath(); ctx.moveTo(0, yy); ctx.lineTo(W, yy); ctx.stroke();
}
var g = ctx.createRadialGradient(W / 2, horizon, 0, W / 2, horizon, 170);
g.addColorStop(0, "rgba(255,70,170,0.5)"); g.addColorStop(1, "rgba(0,0,0,0)");
ctx.fillStyle = g; ctx.fillRect(W / 2 - 170, horizon - 170, 340, 340);
};
};
var active = themes[theme] ? themes[theme]() : themes.cosmos();
var last = performance.now();
var raf;
function loop(now) {
var dt = (now - last) / 1000; last = now;
ctx.clearRect(0, 0, W, H);
active(dt, now / 1000);
raf = requestAnimationFrame(loop);
}
raf = requestAnimationFrame(loop);
document.addEventListener("visibilitychange", function () {
if (document.hidden) { cancelAnimationFrame(raf); }
else { last = performance.now(); raf = requestAnimationFrame(loop); }
});
})();

433
static/style.css Normal file
View File

@@ -0,0 +1,433 @@
/* Supernova — galaxy theme. Slow, cinematic, ordered. */
:root {
--bg: #050510;
--bg2: #0a0a22;
--panel: rgba(20, 18, 48, 0.55);
--panel-border: rgba(130, 110, 255, 0.18);
--nebula-a: #7b2ff7;
--nebula-b: #2b6cff;
--nebula-c: #ff2d95;
--star: #cfe0ff;
--text: #e8eaf6;
--muted: #9aa0c0;
--glow: 0 0 24px rgba(123, 47, 247, 0.35);
--radius: 18px;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
background: var(--bg);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Inter", sans-serif;
min-height: 100%;
-webkit-font-smoothing: antialiased;
}
body { overflow-x: hidden; }
/* ------- background: starfield canvas + nebula ------- */
#starfield {
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
}
.nebula {
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
background:
radial-gradient(ellipse 60% 50% at 15% 20%, rgba(123,47,247,0.28), transparent 60%),
radial-gradient(ellipse 50% 45% at 85% 30%, rgba(43,108,255,0.22), transparent 60%),
radial-gradient(ellipse 55% 50% at 50% 90%, rgba(255,45,149,0.18), transparent 60%),
linear-gradient(180deg, var(--bg) 0%, var(--bg2) 60%, #0a0618 100%);
animation: nebula-drift 40s ease-in-out infinite alternate;
}
@keyframes nebula-drift {
0% { transform: translate(0,0) scale(1); }
100% { transform: translate(0,-2%) scale(1.05); }
}
/* ------- layout ------- */
.wrap {
position: relative;
z-index: 2;
max-width: 1180px;
margin: 0 auto;
padding: 0 24px;
}
/* ------- nav ------- */
nav.top {
position: sticky;
top: 0;
z-index: 10;
backdrop-filter: blur(16px);
background: rgba(8, 8, 24, 0.6);
border-bottom: 1px solid var(--panel-border);
}
nav.top .inner {
max-width: 1180px;
margin: 0 auto;
padding: 14px 24px;
display: flex;
align-items: center;
gap: 22px;
}
.brand {
font-weight: 800;
font-size: 1.25rem;
letter-spacing: 0.5px;
text-decoration: none;
color: var(--text);
display: flex;
align-items: center;
gap: 9px;
}
.brand .orb {
width: 20px; height: 20px; border-radius: 50%;
background: radial-gradient(circle at 35% 35%, #e6d0ff, var(--nebula-a) 60%, var(--nebula-c));
box-shadow: var(--glow);
}
.navlinks { margin-left: auto; display: flex; align-items: center; gap: 18px; }
.navlinks a {
color: var(--muted);
text-decoration: none;
font-size: 0.92rem;
font-weight: 600;
transition: color .2s;
}
.navlinks a:hover { color: var(--text); }
.balance-pill {
background: var(--panel);
border: 1px solid var(--panel-border);
padding: 6px 13px;
border-radius: 999px;
font-weight: 700;
font-size: 0.9rem;
color: #7cffb0;
}
/* ------- buttons ------- */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
border: none;
cursor: pointer;
text-decoration: none;
font-weight: 700;
font-size: 0.98rem;
padding: 12px 26px;
border-radius: 12px;
color: #fff;
background: linear-gradient(135deg, var(--nebula-a), var(--nebula-b));
box-shadow: 0 4px 24px rgba(123,47,247,0.4);
transition: transform .18s ease, box-shadow .18s ease;
}
.btn:hover { transform: translateY(-2px); box-shadow: 0 8px 34px rgba(123,47,247,0.55); }
.btn.ghost {
background: transparent;
border: 1px solid var(--panel-border);
box-shadow: none;
color: var(--text);
}
.btn.ghost:hover { background: rgba(123,47,247,0.12); }
.btn.sm { padding: 8px 16px; font-size: 0.86rem; }
/* ------- hero ------- */
.hero {
padding: 88px 0 72px;
text-align: center;
}
.hero h1 {
font-size: clamp(2.4rem, 6vw, 4.4rem);
margin: 0 0 18px;
line-height: 1.05;
font-weight: 900;
letter-spacing: -0.5px;
background: linear-gradient(120deg, #ffffff, #cfc4ff 40%, var(--nebula-c));
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.hero p.sub {
font-size: 1.18rem;
color: var(--muted);
max-width: 640px;
margin: 0 auto 32px;
line-height: 1.6;
}
.hero .cta { display: flex; gap: 14px; justify-content: center; flex-wrap: wrap; }
/* ------- stat strip ------- */
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 16px;
margin: 48px 0;
}
.stat {
background: var(--panel);
border: 1px solid var(--panel-border);
border-radius: var(--radius);
padding: 22px;
text-align: center;
backdrop-filter: blur(8px);
}
.stat .n { font-size: 1.9rem; font-weight: 900; color: #fff; }
.stat .l { color: var(--muted); font-size: 0.88rem; margin-top: 4px; }
/* ------- platform grid (services) ------- */
.section-title {
font-size: 1.7rem;
font-weight: 800;
margin: 40px 0 18px;
}
.platform-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 18px;
}
.card {
background: var(--panel);
border: 1px solid var(--panel-border);
border-radius: var(--radius);
padding: 24px;
backdrop-filter: blur(8px);
transition: transform .22s ease, box-shadow .22s ease, border-color .22s;
}
.card:hover {
transform: translateY(-4px);
border-color: rgba(130,110,255,0.45);
box-shadow: var(--glow);
}
.card h3 { margin: 0 0 8px; font-size: 1.15rem; }
.card .meta { color: var(--muted); font-size: 0.86rem; }
.card .price {
font-size: 1.35rem;
font-weight: 800;
color: #7cffb0;
margin: 10px 0;
}
.card .row { display: flex; align-items: center; justify-content: space-between; margin-top: 12px; }
/* platform icon "planets" */
.planet {
width: 44px; height: 44px; border-radius: 50%;
display: inline-flex; align-items: center; justify-content: center;
font-size: 1.4rem;
box-shadow: inset -4px -4px 10px rgba(0,0,0,0.35);
margin-bottom: 12px;
}
/* ------- forms ------- */
.field { margin-bottom: 18px; }
.field label { display: block; color: var(--muted); font-size: 0.86rem; font-weight: 600; margin-bottom: 6px; }
.field input {
width: 100%;
padding: 13px 15px;
border-radius: 11px;
border: 1px solid var(--panel-border);
background: rgba(10,10,30,0.6);
color: var(--text);
font-size: 1rem;
}
.field input:focus { outline: none; border-color: var(--nebula-b); box-shadow: 0 0 0 3px rgba(43,108,255,0.2); }
/* ------- slider ------- */
.slider-wrap { margin: 22px 0; }
input[type=range] {
-webkit-appearance: none; appearance: none;
width: 100%; height: 8px; border-radius: 999px;
background: linear-gradient(90deg, var(--nebula-a), var(--nebula-b));
outline: none;
}
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none; appearance: none;
width: 26px; height: 26px; border-radius: 50%;
background: #fff;
border: 4px solid var(--nebula-c);
cursor: pointer;
box-shadow: var(--glow);
}
.live-price {
text-align: center;
font-size: 2rem;
font-weight: 900;
color: #7cffb0;
margin: 12px 0;
}
.live-price small { color: var(--muted); font-size: 0.85rem; font-weight: 600; }
/* ------- tables ------- */
table.list { width: 100%; border-collapse: collapse; }
table.list th, table.list td {
text-align: left; padding: 13px 14px;
border-bottom: 1px solid var(--panel-border);
font-size: 0.94rem;
}
table.list th { color: var(--muted); font-weight: 700; font-size: 0.82rem; text-transform: uppercase; letter-spacing: 0.4px; }
.badge {
display: inline-block; padding: 3px 11px; border-radius: 999px;
font-size: 0.78rem; font-weight: 700;
}
.badge.pending { background: rgba(255,200,80,0.18); color: #ffd36b; }
.badge.completed{ background: rgba(124,255,176,0.18); color: #7cffb0; }
.badge.credited { background: rgba(124,255,176,0.18); color: #7cffb0; }
.badge.inprogress{ background: rgba(90,160,255,0.18); color: #7cc4ff; }
.badge.canceled { background: rgba(255,120,120,0.18); color: #ff9a9a; }
/* ------- flash ------- */
.flash {
padding: 13px 18px; border-radius: 11px; margin: 16px 0;
font-weight: 600; font-size: 0.94rem;
}
.flash.error { background: rgba(255,90,90,0.16); border: 1px solid rgba(255,90,90,0.35); color: #ffb3b3; }
.flash.success { background: rgba(124,255,176,0.14); border: 1px solid rgba(124,255,176,0.35); color: #a4ffc9; }
/* ------- auth card ------- */
.auth-card { max-width: 420px; margin: 60px auto; }
/* ------- footer ------- */
footer.site {
position: relative; z-index: 2;
margin-top: 80px;
padding: 34px 0;
text-align: center;
color: var(--muted);
border-top: 1px solid var(--panel-border);
font-size: 0.88rem;
}
footer.site a { color: var(--nebula-b); text-decoration: none; font-weight: 600; }
.muted { color: var(--muted); }
.center { text-align: center; }
.mt-2 { margin-top: 20px; }
/* ------- interactivity: press, tilt, reveal, ripple, launch ------- */
.btn:active {
transform: translateY(1px) scale(0.97);
box-shadow: 0 2px 12px rgba(123,47,247,0.4);
}
@keyframes ripple {
from { transform: scale(0); opacity: 0.5; }
to { transform: scale(2.4); opacity: 0; }
}
/* card tilt smoothing — gentle, cinematic */
.card {
transition: transform .25s cubic-bezier(.22,.68,.43,1), box-shadow .22s ease, border-color .22s ease;
will-change: transform;
}
.card.reveal { transition: opacity .7s ease, transform .7s ease, box-shadow .22s ease, border-color .22s ease; }
/* scroll reveal */
.reveal { opacity: 0; transform: translateY(16px); }
.reveal.revealed { opacity: 1; transform: translateY(0); }
/* launch button — pulsing "ready to go" glow */
@keyframes pulse-glow {
0%, 100% { box-shadow: 0 4px 24px rgba(123,47,247,0.4); }
50% { box-shadow: 0 4px 44px rgba(255,45,149,0.6); }
}
.btn.launch { animation: pulse-glow 2.6s ease-in-out infinite; }
.btn.launch .rocket { display: inline-block; transition: transform .2s ease; }
.btn.launch:hover .rocket { transform: translateY(-3px) translateX(2px); }
.btn.launch:active .rocket { transform: translateY(6px); }
/* launch celebration banner */
#launch-banner {
position: fixed; top: 20px; left: 50%; transform: translateX(-50%) translateY(-80px);
z-index: 10000;
background: linear-gradient(135deg, var(--nebula-a), var(--nebula-c));
color: #fff; font-weight: 800; padding: 14px 28px; border-radius: 999px;
box-shadow: 0 8px 40px rgba(255,45,149,0.5);
opacity: 0; pointer-events: none;
transition: transform .5s cubic-bezier(.22,1,.36,1), opacity .4s ease;
}
#launch-banner.show { transform: translateX(-50%) translateY(0); opacity: 1; }
/* ------- orbital planet picker ------- */
.orbit-wrap {
position: relative;
width: 100%;
max-width: 480px;
height: 440px;
margin: 12px auto 0;
display: flex;
align-items: center;
justify-content: center;
}
.orbit-sun {
width: 92px; height: 92px;
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 2.5rem;
background: radial-gradient(circle at 35% 35%, #fff, var(--nebula-a) 50%, var(--nebula-c));
box-shadow: 0 0 50px rgba(123,47,247,0.7), 0 0 90px rgba(255,45,149,0.4);
z-index: 2;
animation: sun-pulse 4s ease-in-out infinite;
}
@keyframes sun-pulse {
0%, 100% { box-shadow: 0 0 40px rgba(123,47,247,0.6), 0 0 80px rgba(255,45,149,0.35); }
50% { box-shadow: 0 0 62px rgba(123,47,247,0.85), 0 0 115px rgba(255,45,149,0.55); }
}
.orbit-ring { position: absolute; inset: 0; }
.orbit-ring::before {
content: "";
position: absolute; top: 50%; left: 50%;
width: 350px; height: 350px;
transform: translate(-50%, -50%);
border-radius: 50%;
border: 1px solid rgba(130,110,255,0.16);
}
.planet-orb {
position: absolute; top: 50%; left: 50%;
width: 64px; height: 64px;
margin: -32px 0 0 -32px;
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
text-decoration: none;
background: radial-gradient(circle at 35% 35%, #3b2b8a, #150d2c 72%);
box-shadow: inset -4px -4px 10px rgba(0,0,0,0.45), 0 0 18px rgba(123,47,247,0.35);
transition: box-shadow .3s ease, background .3s ease;
will-change: transform;
}
.planet-orb .po-emoji { font-size: 1.5rem; pointer-events: none; }
.planet-orb .po-name {
position: absolute; top: 100%; left: 50%;
transform: translateX(-50%) translateY(6px);
white-space: nowrap;
font-size: 0.75rem; font-weight: 700; color: var(--muted);
opacity: 0; transition: opacity .25s ease, color .25s ease;
pointer-events: none;
}
.planet-orb:hover { box-shadow: inset -4px -4px 10px rgba(0,0,0,0.45), 0 0 30px rgba(255,45,149,0.65); z-index: 3; }
.planet-orb:hover .po-name { opacity: 1; color: var(--text); }
/* ------- delivery ticker ------- */
.tick { margin-bottom: 16px; }
.tick-top { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; margin-bottom: 6px; }
.tick-name { font-weight: 700; font-size: 0.92rem; }
.tick-meta { color: var(--muted); font-size: 0.8rem; white-space: nowrap; }
.tick-bar {
height: 10px; border-radius: 999px;
background: rgba(10,10,30,0.6);
border: 1px solid var(--panel-border);
overflow: hidden;
}
.tick-fill {
height: 100%;
background: linear-gradient(90deg, var(--nebula-a), var(--nebula-c));
border-radius: 999px;
transition: width 1s cubic-bezier(.22,1,.36,1);
}

12
supernova.nginx Normal file
View File

@@ -0,0 +1,12 @@
server {
listen 80;
server_name supernova.thetempleofdoom.com 10.30.20.203;
location / {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

13
supernova.service Normal file
View File

@@ -0,0 +1,13 @@
[Unit]
Description=Supernova SMM Reseller Panel
After=network.target
[Service]
WorkingDirectory=/opt/supernova
EnvironmentFile=/opt/supernova/.env
ExecStart=/usr/bin/python3 /opt/supernova/app.py
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target

28
templates/auth.html Normal file
View File

@@ -0,0 +1,28 @@
{% extends "base.html" %}
{% block title %}{{ "Sign up" if mode == "signup" else "Sign in" }} — Supernova{% endblock %}
{% block content %}
<div class="auth-card">
<div class="card">
<h3 style="font-size:1.4rem">{{ "Create your account" if mode == "signup" else "Welcome back" }}</h3>
<div class="meta">Pay in Bitcoin. Boost any platform.</div>
<form method="POST" style="margin-top:20px">
<div class="field">
<label>Email</label>
<input type="email" name="email" required placeholder="you@example.com">
</div>
<div class="field">
<label>Password</label>
<input type="password" name="password" required minlength="8" placeholder="••••••••">
</div>
<button class="btn" type="submit" style="width:100%">{{ "Sign up" if mode == "signup" else "Sign in" }}</button>
</form>
<div class="meta center mt-2">
{% if mode == "signup" %}
Already have an account? <a href="{{ url_for('login') }}" style="color:var(--nebula-b)">Sign in</a>
{% else %}
New here? <a href="{{ url_for('signup') }}" style="color:var(--nebula-b)">Create account</a>
{% endif %}
</div>
</div>
</div>
{% endblock %}

97
templates/base.html Normal file
View File

@@ -0,0 +1,97 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}Supernova — Buy Instagram, TikTok & YouTube Followers Cheap | Bitcoin SMM Panel{% endblock %}</title>
<meta name="description" content="{% block description %}Buy cheap Instagram, TikTok, YouTube, Facebook & X followers, likes and views from $0.02 per 1k. Pay with Bitcoin, instant automated delivery, 361 services across 18+ platforms.{% endblock %}">
<meta name="keywords" content="buy instagram followers, buy tiktok followers, buy youtube views, buy followers cheap, smm panel, social media marketing panel, buy likes, buy views, buy facebook followers, buy x twitter followers, bitcoin smm panel">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://supernova.thetempleofdoom.com/">
<!-- Open Graph -->
<meta property="og:type" content="website">
<meta property="og:site_name" content="Supernova">
<meta property="og:title" content="Supernova — Buy Instagram, TikTok & YouTube Followers Cheap | Bitcoin SMM Panel">
<meta property="og:description" content="Buy cheap followers, likes and views for Instagram, TikTok, YouTube, Facebook & X. Pay with Bitcoin. Instant automated delivery. 361 services, 18+ platforms.">
<meta property="og:url" content="https://supernova.thetempleofdoom.com/">
<meta property="og:image" content="https://supernova.thetempleofdoom.com/static/og.png">
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Supernova — Buy Instagram, TikTok & YouTube Followers Cheap">
<meta name="twitter:description" content="Buy cheap followers, likes & views. Pay with Bitcoin. Instant automated delivery across 18+ platforms.">
<meta name="twitter:image" content="https://supernova.thetempleofdoom.com/static/og.png">
<!-- JSON-LD -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Supernova",
"url": "https://supernova.thetempleofdoom.com/",
"description": "Bitcoin-powered social media growth panel. Buy Instagram, TikTok, YouTube, Facebook and X followers, likes and views.",
"logo": "https://supernova.thetempleofdoom.com/static/og.png"
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "Supernova",
"url": "https://supernova.thetempleofdoom.com/",
"potentialAction": {
"@type": "SearchAction",
"target": "https://supernova.thetempleofdoom.com/services?q={search_term_string}",
"query-input": "required name=search_term_string"
}
}
</script>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}?v=3">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌌</text></svg>">
<!-- Umami analytics -->
<script async src="https://analytics.thetempleofdoom.com/script.js" data-website-id="953c15df-ba4c-453a-a7c6-465fa9e3f202"></script>
</head>
<body>
<canvas id="starfield"></canvas>
<div class="nebula"></div>
<nav class="top">
<div class="inner">
<a class="brand" href="{{ url_for('index') }}"><span class="orb"></span>Supernova</a>
<div class="navlinks">
<a href="{{ url_for('services') }}">Services</a>
{% if user %}
<a href="{{ url_for('dashboard') }}">Dashboard</a>
<a href="{{ url_for('orders') }}">Orders</a>
<span class="balance-pill">₿ ${{ "%.2f"|format(user["balance"]) }}</span>
<a class="btn sm" href="{{ url_for('deposit') }}">+ Add funds</a>
<a href="{{ url_for('logout') }}">Log out</a>
{% else %}
<a href="{{ url_for('login') }}">Sign in</a>
<a class="btn sm" href="{{ url_for('signup') }}">Get started</a>
{% endif %}
</div>
</div>
</nav>
<main class="wrap">
{% with msgs = get_flashed_messages(with_categories=true) %}
{% for cat, m in msgs %}
<div class="flash {{ cat }}">{{ m }}</div>
{% endfor %}
{% endwith %}
{% block content %}{% endblock %}
</main>
<footer class="site">
<div class="wrap">
Powered by Supernova · <a href="https://buymeacoffee.com/r26xrthzttg" target="_blank" rel="noopener">☕ Support this project</a>
</div>
</footer>
<script src="{{ url_for('static', filename='app.js') }}?v=3"></script>
</body>
</html>

72
templates/bg-demo.html Normal file
View File

@@ -0,0 +1,72 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Cosmic Backgrounds — theme gallery</title>
<style>
* { box-sizing: border-box; }
body { margin: 0; min-height: 100vh; font-family: -apple-system, "Segoe UI", Roboto, sans-serif; color: #e8eaf6; background: #050510; }
.overlay {
position: relative; z-index: 2;
max-width: 900px; margin: 0 auto; padding: 60px 24px;
}
h1 { font-size: 2rem; margin: 0 0 6px; }
.sub { color: #9aa0c0; margin: 0 0 28px; }
.picker { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 40px; }
.picker a {
text-decoration: none; color: #cfc4ff; font-weight: 600; font-size: 0.92rem;
padding: 10px 18px; border-radius: 999px;
border: 1px solid rgba(130,110,255,0.3);
background: rgba(20,18,48,0.55);
transition: all .2s ease;
}
.picker a.active, .picker a:hover {
background: linear-gradient(135deg, #7b2ff7, #2b6cff);
color: #fff; border-color: transparent;
box-shadow: 0 4px 20px rgba(123,47,247,0.4);
}
.card {
background: rgba(20,18,48,0.6);
border: 1px solid rgba(130,110,255,0.2);
border-radius: 18px; padding: 28px;
backdrop-filter: blur(8px);
margin-bottom: 16px;
}
.card h2 { margin: 0 0 10px; font-size: 1.2rem; }
.card p { color: #9aa0c0; line-height: 1.7; margin: 0; }
code {
display: block; margin-top: 40px; padding: 14px 18px;
background: rgba(10,10,30,0.7); border: 1px solid rgba(130,110,255,0.2);
border-radius: 10px; font-size: 0.85rem; color: #7cffb0;
overflow-x: auto; white-space: nowrap;
}
</style>
</head>
<body>
<script src="{{ url_for('static', filename='cosmic-bg.js') }}" data-theme="{{ theme }}"></script>
<div class="overlay">
<h1>✨ Cosmic Backgrounds</h1>
<p class="sub">Current theme: <strong>{{ theme }}</strong> — click any to preview. Drop into any site with one script tag.</p>
<div class="picker">
{% for t in themes %}
<a href="{{ url_for('bg_demo') }}?theme={{ t }}" class="{{ 'active' if t == theme else '' }}">{{ t }}</a>
{% endfor %}
</div>
<div class="card">
<h2>This is how your content sits over it</h2>
<p>The background is a fixed, pointer-events-none canvas layered behind everything — your existing site, nav, cards and text all render on top unchanged. Every theme is slow + smooth, and pauses automatically when the tab is in the background (zero wasted CPU).</p>
</div>
<div class="card">
<h2>Add it to any site</h2>
<p>One line in the <code>&lt;head&gt;</code> — swap <code>data-theme</code> to taste:</p>
</div>
<code>&lt;script src="https://supernova.thetempleofdoom.com/static/cosmic-bg.js" data-theme="{{ theme }}"&gt;&lt;/script&gt;</code>
</div>
</body>
</html>

43
templates/dashboard.html Normal file
View File

@@ -0,0 +1,43 @@
{% extends "base.html" %}
{% block title %}Dashboard — Supernova{% endblock %}
{% block content %}
<div class="section-title" style="margin-top:36px">Your command deck</div>
<div class="stats">
<div class="stat"><div class="n">₿ ${{ "%.2f"|format(user.balance) }}</div><div class="l">balance</div></div>
<div class="stat"><div class="n">{{ orders|length }}</div><div class="l">recent orders</div></div>
<div class="stat"><div class="n">⚡</div><div class="l">auto-fulfilled</div></div>
</div>
<div class="platform-grid">
<div class="card">
<h3>Add funds</h3>
<div class="meta">Deposit Bitcoin — credited instantly on confirmation.</div>
<div class="row"><a class="btn sm" href="{{ url_for('deposit') }}">Deposit ₿</a></div>
</div>
<div class="card">
<h3>New boost</h3>
<div class="meta">Browse the catalog and launch a campaign.</div>
<div class="row"><a class="btn sm" href="{{ url_for('services') }}">Explore →</a></div>
</div>
<div class="card">
<h3>Order history</h3>
<div class="meta">Track every boost and its status.</div>
<div class="row"><a class="btn sm ghost" href="{{ url_for('orders') }}">View all</a></div>
</div>
</div>
<div class="section-title">Live delivery</div>
<div class="card" style="padding:22px">
<div id="ticker"><div class="muted center">Loading your boosts…</div></div>
</div>
{% if deposits %}
<div class="section-title">Recent deposits</div>
<table class="list">
<tr><th>Amount</th><th>Status</th></tr>
{% for d in deposits %}
<tr><td>${{ "%.2f"|format(d.amount) }}</td><td><span class="badge {{ d.status }}">{{ d.status }}</span></td></tr>
{% endfor %}
</table>
{% endif %}
{% endblock %}

18
templates/deposit.html Normal file
View File

@@ -0,0 +1,18 @@
{% extends "base.html" %}
{% block title %}Deposit — Supernova{% endblock %}
{% block content %}
<div class="auth-card">
<div class="card">
<h3 style="font-size:1.4rem">Add Bitcoin funds</h3>
<div class="meta">Current balance: <strong style="color:#7cffb0">${{ "%.2f"|format(user.balance) }}</strong>. Deposits credit automatically once the BTC confirms.</div>
<form method="POST" style="margin-top:20px">
<div class="field">
<label>Amount (USD)</label>
<input type="number" name="amount" step="0.01" min="0.50" required placeholder="25.00">
</div>
<button class="btn" type="submit" style="width:100%">⚡ Generate ₿ invoice</button>
</form>
<div class="meta center mt-2">You'll be redirected to a Bitcoin checkout page.</div>
</div>
</div>
{% endblock %}

69
templates/index.html Normal file
View File

@@ -0,0 +1,69 @@
{% extends "base.html" %}
{% block title %}Supernova — Buy Instagram, TikTok & YouTube Followers Cheap | Bitcoin SMM Panel{% endblock %}
{% block content %}
<div class="hero">
<h1>Grow your reach<br>across the galaxy</h1>
<p class="sub">Followers, likes and views for every platform — paid in Bitcoin, delivered to your handle. Pick your world, choose your boost, watch it rise.</p>
<div class="cta">
<a class="btn" href="{{ url_for('services') }}">🌌 Explore services</a>
{% if not user %}<a class="btn ghost" href="{{ url_for('signup') }}">Create account</a>{% endif %}
</div>
</div>
<div class="stats">
<div class="stat"><div class="n">361</div><div class="l">services</div></div>
<div class="stat"><div class="n">18+</div><div class="l">platforms</div></div>
<div class="stat"><div class="n">₿</div><div class="l">Bitcoin payments</div></div>
<div class="stat"><div class="n">100%</div><div class="l">automated</div></div>
</div>
<div class="section-title">Spin the galaxy — choose your world</div>
<div class="orbit-wrap">
<div class="orbit-sun">🌌</div>
<div class="orbit-ring" id="orbit">
{% for name, icon in [("Instagram","📸"),("TikTok","🎵"),("YouTube","▶️"),("X / Twitter","🐦"),("Facebook","👥"),("Spotify","🎧"),("Telegram","✈️"),("Snapchat","👻")] %}
<a class="planet-orb" href="{{ url_for('services') }}" data-i="{{ loop.index0 }}" data-name="{{ name }}">
<span class="po-emoji">{{ icon }}</span>
<span class="po-name">{{ name }}</span>
</a>
{% endfor %}
</div>
</div>
<div class="center muted" style="margin-top:10px">← drag to spin · click a planet to explore →</div>
<section class="seo-block">
<h2>Buy followers, likes & views for every social platform</h2>
<p>Supernova is a <strong>Bitcoin-powered social media marketing panel</strong> — the fastest way to grow your presence on <a href="{{ url_for('services') }}">Instagram</a>, TikTok, YouTube, Facebook, X (Twitter), Spotify, Telegram and more. With <strong>361 services</strong> starting from just <strong>$0.02 per 1,000</strong>, you can buy exactly the engagement you need and watch it land automatically.</p>
<h3>Instagram</h3>
<p>Buy Instagram followers, likes, views, reel views and comments. From lifetime likes to real low-drop followers and country-targeted growth — boost your profile and look established overnight.</p>
<h3>TikTok</h3>
<p>Buy TikTok followers, likes, views, shares and saves. Kickstart the algorithm and push your videos into the For You feed with fast, drip-fed delivery.</p>
<h3>YouTube</h3>
<p>Buy YouTube views, subscribers, likes and watch-time. Grow your channel's authority and signal relevance to YouTube's recommendation engine.</p>
<h3>X (Twitter) & Facebook</h3>
<p>Buy X followers, retweets, likes and impressions — plus Facebook page likes, followers, comments and live-stream viewers.</p>
<h3>Why Supernova</h3>
<ul>
<li><strong>Pay with Bitcoin</strong> — private, no KYC, instant credit.</li>
<li><strong>Fully automated</strong> — order and delivery starts in minutes.</li>
<li><strong>Cheap & scalable</strong> — from a few hundred to millions of units.</li>
<li><strong>Refill & drip-feed</strong> — services with lifetime refill and gradual delivery.</li>
</ul>
<p class="cta-line"><a class="btn" href="{{ url_for('services') }}">Browse all 361 services →</a></p>
</section>
<style>
.seo-block { margin-top: 64px; padding: 8px 0 24px; }
.seo-block h2 { font-size: 1.6rem; font-weight: 800; }
.seo-block h3 { font-size: 1.15rem; margin-top: 26px; }
.seo-block p { color: var(--muted); line-height: 1.7; }
.seo-block a { color: var(--nebula-b); }
.seo-block ul { color: var(--muted); line-height: 1.8; }
.seo-block .cta-line { margin-top: 28px; }
</style>
{% endblock %}

29
templates/order.html Normal file
View File

@@ -0,0 +1,29 @@
{% extends "base.html" %}
{% block title %}Order {{ s.name }} — Supernova{% endblock %}
{% block content %}
<div class="auth-card" style="max-width:560px">
<div class="card">
<h3 style="font-size:1.4rem;margin-bottom:6px">{{ s.name }}</h3>
<div class="meta">Category: {{ s.category }} · rate ${{ "%.2f"|format(s.rate|float) }}/1k · min {{ s.min }} · max {{ "{:,}".format(s.max) }}</div>
<form method="POST" action="{{ url_for('order', service_id=s.service) }}">
<div class="field" style="margin-top:22px">
<label>Target link / username</label>
<input type="text" name="link" placeholder="https://instagram.com/yourhandle" required>
</div>
<div class="slider-wrap">
<label>Quantity</label>
<div class="live-price" id="live-price">$0.00</div>
<input type="range" id="qty" name="quantity" data-service="{{ s.service }}"
min="{{ s.min }}" max="{{ [s.max, 50000]|min }}" step="{{ [1, s.min//10]|max }}">
<div class="meta center" style="margin-top:10px"><span id="qty-val">{{ s.min }}</span> units</div>
</div>
<button class="btn launch" type="submit" style="width:100%;margin-top:14px"><span class="rocket">🚀</span> Launch boost</button>
</form>
<div class="meta center mt-2">Charged from your balance. {% if s.refill %}♻️ Auto-refill included.{% endif %}</div>
</div>
</div>
{% endblock %}

25
templates/orders.html Normal file
View File

@@ -0,0 +1,25 @@
{% extends "base.html" %}
{% block title %}Orders — Supernova{% endblock %}
{% block content %}
<div id="launch-banner">🚀 Boost launched — delivery starting!</div>
<div class="section-title" style="margin-top:36px">Your boosts</div>
{% if orders %}
<table class="list">
<tr><th>Service</th><th>Link</th><th>Qty</th><th>Cost</th><th>Status</th></tr>
{% for o in orders %}
<tr>
<td>{{ o.service_name }}</td>
<td class="muted" style="max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ o.link }}</td>
<td>{{ "{:,}".format(o.quantity) }}</td>
<td>${{ "%.2f"|format(o.cost) }}</td>
<td><span class="badge {{ o.status }}">{{ o.status }}</span></td>
</tr>
{% endfor %}
</table>
{% else %}
<div class="card center" style="padding:40px">
<div style="font-size:2rem">🪐</div>
<div class="muted mt-2">No orders yet. <a href="{{ url_for('services') }}" style="color:var(--nebula-b)">Launch your first boost →</a></div>
</div>
{% endif %}
{% endblock %}

57
templates/services.html Normal file
View File

@@ -0,0 +1,57 @@
{% extends "base.html" %}
{% block title %}Services — Supernova{% endblock %}
{% block content %}
<div class="hero" style="padding:48px 0 24px">
<h1 style="font-size:2.4rem">Services</h1>
<p class="sub">Search the catalog. Every boost is fulfilled automatically.</p>
<div class="field" style="max-width:520px;margin:0 auto">
<input id="search" type="text" placeholder="Search — e.g. Instagram likes, TikTok views…" autocomplete="off">
</div>
</div>
<div id="catalog">
{% for cat, items in cats.items() %}
<section class="cat-block" data-cat="{{ cat|lower }}">
<h2 class="section-title">{{ cat }} <span class="muted" style="font-size:0.9rem">({{ items|length }})</span></h2>
<div class="platform-grid">
{% for s in items %}
<div class="card" data-name="{{ s.name|lower }}">
<h3>{{ s.name }}</h3>
<div class="meta">min {{ s.min }} · max {{ "{:,}".format(s.max) }}</div>
<div class="price">${{ "%.2f"|format(s.rate|float) }} <span class="muted" style="font-size:0.8rem">/ 1k</span></div>
<div class="row">
<span class="meta">{% if s.refill %}♻️ refill{% endif %}{% if s.dripfeed %} 💧 dripfeed{% endif %}{% if not s.refill and not s.dripfeed %}·{% endif %}</span>
<a class="btn sm" href="{{ url_for('order', service_id=s.service) }}">Boost →</a>
</div>
</div>
{% endfor %}
</div>
</section>
{% endfor %}
</div>
<div id="no-results" class="center muted" style="display:none;padding:40px 0">No services match your search.</div>
<script>
(function () {
const search = document.getElementById('search');
const cards = document.querySelectorAll('.card[data-name]');
const blocks = document.querySelectorAll('.cat-block');
const noRes = document.getElementById('no-results');
search.addEventListener('input', () => {
const q = search.value.trim().toLowerCase();
let any = false;
cards.forEach(c => {
const hit = !q || c.dataset.name.includes(q);
c.style.display = hit ? '' : 'none';
if (hit) any = true;
});
blocks.forEach(b => {
const has = Array.from(b.querySelectorAll('.card[data-name]')).some(c => c.style.display !== 'none');
b.style.display = has ? '' : 'none';
});
noRes.style.display = any ? 'none' : '';
});
})();
</script>
{% endblock %}