commit f9242710ebf4114d013ec9528f4aa961595beaf5 Author: drjones Date: Tue Sep 1 01:37:39 2026 -0700 Pest Nocturne β€” WSU EM019 study suite (freemium BTCPay paywall) diff --git a/README.md b/README.md new file mode 100644 index 0000000..af15644 --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# Pest Nocturne πŸͺ² + +A midnight specimen-cabinet study suite for the **WSU EM019** pest management +certification exam. The complete *Study Manual for Pest Management +Professionals* rebuilt as an interactive, illustrated single-page app. + +## Live +**https://nocturne.thetempleofdoom.com** + +## What's inside +- **Field guide** β€” all 13 chapters with illustrated plates +- **Identification keys** β€” branching keys, walked one couplet at a time +- **House faults** β€” 22 points of faulty construction/maintenance, in order +- **Flashcards** πŸ”’ β€” premium +- **Practice exam** πŸ”’ β€” premium (scored, with the official answer key) +- **Glossary** β€” every defined term, searchable as you type + +## Monetization +Freemium: the field guide, keys, and house faults are free. **Flashcards and +the practice exam are a $5 Bitcoin unlock** via BTCPay. + +- BTCPay store: `FDT6DHWEaA7DF5WFePkp4pufgQ9yy1G6JfzTCwey1jYn` +- Price: $5 USD (on-chain BTC) +- Flow: `/unlock` β†’ BTCPay invoice β†’ `/webhook/btcpay` (InvoiceSettled) β†’ client polls `/api/status` β†’ unlocks + +## Stack +- Single self-contained `index.html` (vanilla JS SPA, no build step) +- Flask (`app.py`) for the paywall + BTCPay webhook +- nginx reverse proxy β†’ Flask :5000 +- SQLite (`orders.db`) for order state + +## Deploy +- Proxmox LXC CT **117** (`nocturne`) @ 10.30.20.234 +- systemd `nocturne.service`, nginx `sites-enabled/nocturne` +- Cloudflare tunnel `thetemple-fleet` (`1aeb1ac0`), ingress β†’ `:80` + +## Files +- `index.html` β€” the study suite + injected paywall gate + BMAC footer +- `app.py` β€” Flask backend (BTCPay invoice, webhook, unlock status) +- `inject.py` β€” one-shot script that injects the paywall + footer into the HTML +- `nocturne.service` / `nocturne.nginx` β€” service + nginx config diff --git a/app.py b/app.py new file mode 100644 index 0000000..23422ea --- /dev/null +++ b/app.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +# Pest Nocturne β€” WSU EM019 study suite, freemium paywall (BTCPay) +import os, json, uuid, time, sqlite3, hmac, hashlib +from flask import Flask, request, jsonify, send_from_directory, redirect + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +DB_PATH = os.path.join(BASE_DIR, "orders.db") +HTML = os.path.join(BASE_DIR, "index.html") + +# BTCPay config (store FDT6DHWEaA7DF5WFePkp4pufgQ9yy1G6JfzTCwey1jYn) +BTCPAY_URL = os.environ.get("BTCPAY_URL", "https://10.30.20.140") +BTCPAY_STORE = os.environ.get("BTCPAY_STORE", "FDT6DHWEaA7DF5WFePkp4pufgQ9yy1G6JfzTCwey1jYn") +BTCPAY_KEY = os.environ.get("BTCPAY_KEY", "206f08d0a8efc4cfbe524188756c5bd1b2fd277a") +PRICE_USD = os.environ.get("PRICE_USD", "5.00") + +app = Flask(__name__) + +def db(): + c = sqlite3.connect(DB_PATH) + c.execute("CREATE TABLE IF NOT EXISTS orders (id TEXT PRIMARY KEY, created REAL, paid INTEGER DEFAULT 0)") + c.commit() + return c + +def create_btcpay_invoice(order_id): + """Create a BTCPay invoice and return its checkout URL, or None.""" + import urllib.request, ssl + ctx = ssl.create_default_context() + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE # BTCPay uses a self-signed cert + payload = json.dumps({ + "amount": PRICE_USD, + "currency": "USD", + "metadata": {"orderId": order_id}, + "checkout": {"redirectURL": f"/?paid=1&order={order_id}"}, + }).encode() + req = urllib.request.Request( + f"{BTCPAY_URL}/api/v1/stores/{BTCPAY_STORE}/invoices", + data=payload, method="POST", + headers={"Authorization": f"token {BTCPAY_KEY}", "Content-Type": "application/json"}, + ) + try: + with urllib.request.urlopen(req, timeout=20, context=ctx) as r: + data = json.loads(r.read()) + return data.get("checkoutLink") or data.get("checkout_url") + except Exception as e: + app.logger.error(f"BTCPay invoice error: {e}") + return None + +@app.route("/") +def index(): + return send_from_directory(BASE_DIR, "index.html") + +@app.route("/api/status") +def api_status(): + oid = request.args.get("order", "") + c = db() + row = c.execute("SELECT paid FROM orders WHERE id=?", (oid,)).fetchone() + c.close() + return jsonify({"paid": bool(row and row[0])}) + +@app.route("/unlock", methods=["POST"]) +def unlock(): + order_id = uuid.uuid4().hex[:16] + c = db() + c.execute("INSERT INTO orders (id, created, paid) VALUES (?,?,0)", (order_id, time.time())) + c.commit() + c.close() + checkout_url = create_btcpay_invoice(order_id) + if not checkout_url: + return jsonify({"error": "could not create invoice"}), 502 + return jsonify({"order_id": order_id, "checkout_url": checkout_url}) + +@app.route("/webhook/btcpay", methods=["POST"]) +def webhook(): + raw = request.get_data() + data = json.loads(raw) if raw else {} + etype = data.get("type", "") + order_id = (data.get("metadata") or {}).get("orderId") + if etype == "InvoiceSettled" and order_id: + c = db() + c.execute("UPDATE orders SET paid=1 WHERE id=?", (order_id,)) + c.commit() + c.close() + app.logger.info(f"order {order_id} settled") + return jsonify({"ok": True}) + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5000) diff --git a/index.html b/index.html new file mode 100644 index 0000000..36aba3a --- /dev/null +++ b/index.html @@ -0,0 +1,2373 @@ +Pest Nocturne + + + + + + + +
+ +
+
+
+
+ + Pest Nocturne +
+ + +
+
+ +
+
+
+
+
+
+
+
+
+ +
+ Built from EM019 β€” Study Manual for Pest Management Professionals, WSU Extension (rev. April 2021).
+ Antonelli, Whitworth, Suomi, Black, Foss & Maguire. A study aid, never a substitute for the label or the WAC. +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ Pest Nocturne — a study suite for WSU EM019 · Buy me a coffee +
+ diff --git a/inject.py b/inject.py new file mode 100644 index 0000000..24e0ac9 --- /dev/null +++ b/inject.py @@ -0,0 +1,93 @@ +import io + +p = "index.html" +s = open(p).read() + +PAYWALL = r''' + + + + +''' + +# The file has no β€” it's an HTML fragment ending in . Append at EOF. +BMAC = r''' +
+ Pest Nocturne — a study suite for WSU EM019 · Buy me a coffee +
+''' +s = s.rstrip() + '\n' + PAYWALL + '\n' + BMAC + '\n' + +open(p, 'w').write(s) +print(f"injected paywall + BMAC footer; new size {len(s)} bytes") diff --git a/nocturne.nginx b/nocturne.nginx new file mode 100644 index 0000000..7e8a942 --- /dev/null +++ b/nocturne.nginx @@ -0,0 +1,12 @@ +server { + listen 80; + server_name _; + + 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; + } +} diff --git a/nocturne.service b/nocturne.service new file mode 100644 index 0000000..be84886 --- /dev/null +++ b/nocturne.service @@ -0,0 +1,13 @@ +[Unit] +Description=Pest Nocturne study suite (Flask) +After=network.target + +[Service] +WorkingDirectory=/opt/nocturne +ExecStart=/usr/bin/python3 /opt/nocturne/app.py +Restart=always +RestartSec=3 +Environment=PYTHONUNBUFFERED=1 + +[Install] +WantedBy=multi-user.target