🚀 Initial release — Daily App Factory v7

This commit is contained in:
drjones
2026-08-18 07:02:04 -07:00
commit 8bb1ba4288
5 changed files with 797 additions and 0 deletions

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Daily App Factory (drjones)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

57
README.md Normal file
View File

@@ -0,0 +1,57 @@
# InterviewForge
> Generate tailored interview questions in seconds.
InterviewForge takes a job description and forges a complete, role-specific interview
question set — technical, behavioral (STAR), and follow-up questions — in under a second.
No AI API, no cloud, no data leaves your machine.
## Features
- 🎯 **Tailored to the role** — detects skills in the job description (Python, AWS, Docker,
SQL, React, sales, PM, leadership…) and serves matching technical questions
- 🧠 **STAR behavioral set** — 8 proven behavioral questions, 3 sampled per run
- 🔁 **Follow-up/clarifying questions** — interview the interviewer with precise follow-ups
-**Pay with Bitcoin** — one-time $9 premium via BTCPay Server
- 🕵️ **Zero backend** — pure Python stdlib + Flask, no external APIs
## Endpoints
| Route | Auth | Description |
|-------|------|-------------|
| `/` | — | Landing page |
| `/health` | — | Health check (JSON) |
| `/pricing` | — | Free vs Premium |
| `/register` | — | Create account |
| `/login` | — | Login |
| `/dashboard` | ✅ | Core tool: paste JD, forge questions |
| `/process` | ✅ | POST `input_data` → question set |
| `/checkout``/checkout/create` | ✅ | BTCPay invoice (one-time $9) |
| `/webhook/btcpay` | — | BTCPay webhook (settles payments) |
| `/payment/success`, `/payment/cancel` | ✅ | Checkout transitions |
| `/about`, `/sitemap.xml` | — | SEO |
## Tech Stack
Flask · SQLite (WAL) · BTCPay Server (Greenfield API) · nginx reverse proxy ·
systemd `interviewforge.service` · Python 3.9-compatible (stdlib only)
## Deployment
- **Host**: Proxmox LXC container (Debian 12, host: `proxmox` on 10.30.20.85)
- **App dir**: `/opt/interviewforge`
- **Service**: `systemctl restart interviewforge`
- **nginx**: port 80 → `127.0.0.1:5000`
## Paying
Premium is a **one-time** $9 payment via Bitcoin. The BTCPay webhook
(`InvoiceSettled`) flips the user to premium — no recurring charges.
## License
MIT — see [LICENSE](LICENSE).
**Enjoy InterviewForge?** Support it on [Buy Me a Coffee](https://buymeacoffee.com/r26xrthzttg).
*Built by the Daily App Factory — one new app every day.*

695
app.py Normal file
View File

@@ -0,0 +1,695 @@
#!/usr/bin/env python3
"""
App Factory Base Template — Production-ready Flask app with all monetization built-in.
Customize APP_NAME, APP_SLUG, TAGLINE, PREMIUM_PRICE, and the core_feature() function.
"""
import os
import json
import hashlib
import sqlite3
import secrets
import ssl
import urllib.request
import urllib.parse
from datetime import datetime
from typing import Optional
from flask import Flask, request, redirect, session, jsonify, make_response
# ─── CONFIG ───────────────────────────────────────────────
APP_NAME = "InterviewForge"
APP_SLUG = "interviewforge"
TAGLINE = "Generate tailored interview questions in seconds"
PREMIUM_PRICE = 9 # USD, one-time
PREMIUM_DESCRIPTION = "Premium unlocks unlimited question sets with industry follow-ups"
PRIMARY_COLOR = "#2563EB"
ACCENT_COLOR = "#F59E0B"
# BTCPay (replaced during deployment)
BTC_URL = "https://10.30.20.140"
BTC_STORE = "FEw7ACTcckppRK2KMbZHvS34P96dLQE4ZBVqfTM6qyfR"
BTC_KEY = "68c8ba2c6815d0432ec30fe3cccb8fb3fb04fc5f"
# Database
DB_PATH = f"/opt/{APP_SLUG}/data.db"
app = Flask(__name__)
app.secret_key = secrets.token_hex(32)
# ─── DATABASE ─────────────────────────────────────────────
def get_db():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode=WAL")
return conn
def init_db():
conn = get_db()
conn.executescript("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
is_premium INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now')),
btcpay_invoice_id TEXT,
payment_status TEXT DEFAULT 'none'
);
CREATE TABLE IF NOT EXISTS payments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
invoice_id TEXT UNIQUE,
amount_usd REAL,
status TEXT DEFAULT 'pending',
created_at TEXT DEFAULT (datetime('now')),
settled_at TEXT,
FOREIGN KEY (user_id) REFERENCES users(id)
);
CREATE TABLE IF NOT EXISTS results (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
data TEXT,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (user_id) REFERENCES users(id)
);
""")
conn.commit()
conn.close()
# ─── AUTH HELPERS ─────────────────────────────────────────
def hash_password(password: str) -> str:
salt = "appfactory2026"
return hashlib.sha256((password + salt).encode()).hexdigest()
def login_required(f):
from functools import wraps
@wraps(f)
def decorated(*args, **kwargs):
if 'user_id' not in session:
return redirect('/login?next=' + request.path)
return f(*args, **kwargs)
return decorated
# ─── BTCPAY HELPERS ───────────────────────────────────────
def create_btcpay_invoice(amount_usd: float, order_id: str, description: str) -> Optional[dict]:
try:
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
data = json.dumps({
"amount": str(amount_usd),
"currency": "USD",
"metadata": {"orderId": order_id, "description": description}
}).encode()
req = urllib.request.Request(
f"{BTC_URL}/api/v1/stores/{BTC_STORE}/invoices",
data=data,
headers={"Authorization": f"token {BTC_KEY}", "Content-Type": "application/json"}
)
resp = urllib.request.urlopen(req, context=ctx, timeout=30)
inv = json.loads(resp.read())
return {"id": inv["id"], "checkout_url": inv["checkoutLink"]}
except Exception as e:
app.logger.error(f"BTCPay invoice creation failed: {e}")
return None
def check_btcpay_invoice(invoice_id: str) -> Optional[dict]:
try:
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
req = urllib.request.Request(
f"{BTC_URL}/api/v1/stores/{BTC_STORE}/invoices/{invoice_id}",
headers={"Authorization": f"token {BTC_KEY}"}
)
resp = urllib.request.urlopen(req, context=ctx, timeout=15)
return json.loads(resp.read())
except Exception as e:
app.logger.error(f"BTCPay check failed: {e}")
return None
# ─── TEMPLATES ────────────────────────────────────────────
BASE_STYLE = """
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family:'Inter',sans-serif; background:#18181b; color:#e4e4e7; min-height:100vh; display:flex; flex-direction:column; }
a { color:{{ACCENT_COLOR}}; text-decoration:none; }
a:hover { text-decoration:underline; }
.container { max-width:1100px; margin:0 auto; padding:0 1.5rem; width:100%; }
nav { background:#18181b; border-bottom:1px solid #27272a; padding:1rem 0; position:sticky; top:0; z-index:100; }
nav .container { display:flex; justify-content:space-between; align-items:center; }
nav .logo { font-weight:800; font-size:1.25rem; color:#fff; display:flex; align-items:center; gap:0.5rem; }
nav .links { display:flex; gap:1.5rem; align-items:center; }
nav .links a { color:#a1a1aa; font-size:0.9rem; font-weight:500; transition:color 0.2s; }
nav .links a:hover { color:#fff; text-decoration:none; }
.btn { display:inline-flex; align-items:center; gap:0.4rem; padding:0.6rem 1.4rem; border-radius:8px; font-weight:600; font-size:0.9rem; cursor:pointer; border:none; transition:all 0.2s; text-decoration:none; }
.btn:hover { text-decoration:none; transform:translateY(-1px); }
.btn-primary { background:{{PRIMARY_COLOR}}; color:#fff; }
.btn-primary:hover { box-shadow:0 8px 25px rgba(37,99,235,0.3); }
.btn-outline { background:transparent; border:2px solid #3f3f46; color:#e4e4e7; }
.btn-outline:hover { border-color:{{ACCENT_COLOR}}; }
.card { background:#27272a; border-radius:12px; padding:1.5rem; border:1px solid #3f3f46; }
.hero { padding:5rem 0 4rem; text-align:center; }
.hero h1 { font-size:3rem; font-weight:800; color:#fff; line-height:1.1; margin-bottom:1rem; }
.hero p { font-size:1.2rem; color:#a1a1aa; max-width:600px; margin:0 auto 2rem; line-height:1.6; }
.features { display:grid; grid-template-columns:repeat(auto-fit,minmax(300px,1fr)); gap:1.5rem; padding:3rem 0; }
.feature-card { background:#27272a; border-radius:12px; padding:2rem; border:1px solid #3f3f46; transition:border-color 0.2s; }
.feature-card:hover { border-color:{{ACCENT_COLOR}}; }
.feature-card .icon { font-size:2rem; margin-bottom:1rem; }
.feature-card h3 { color:#fff; margin-bottom:0.5rem; }
.feature-card p { color:#a1a1aa; font-size:0.95rem; line-height:1.5; }
.pricing-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(280px,1fr)); gap:1.5rem; padding:3rem 0; }
.pricing-card { background:#27272a; border-radius:12px; padding:2rem; border:2px solid #3f3f46; text-align:center; }
.pricing-card.premium { border-color:{{ACCENT_COLOR}}; position:relative; }
.pricing-card.premium::before { content:'POPULAR'; position:absolute; top:-12px; left:50%; transform:translateX(-50%); background:{{ACCENT_COLOR}}; color:#fff; padding:0.2rem 1rem; border-radius:20px; font-size:0.75rem; font-weight:700; }
.pricing-card h3 { color:#fff; font-size:1.3rem; margin-bottom:0.5rem; }
.pricing-card .price { font-size:2.5rem; font-weight:800; color:#fff; margin:1rem 0; }
.pricing-card .price span { font-size:1rem; color:#a1a1aa; font-weight:400; }
.pricing-card ul { list-style:none; text-align:left; margin:1.5rem 0; }
.pricing-card ul li { padding:0.5rem 0; color:#a1a1aa; font-size:0.9rem; }
.pricing-card ul li::before { content:''; color:{{ACCENT_COLOR}}; margin-right:0.5rem; font-weight:700; }
footer { margin-top:auto; padding:2rem 0; text-align:center; color:#71717a; font-size:0.85rem; }
.flash { padding:0.8rem 1.5rem; border-radius:8px; margin-bottom:1rem; font-weight:500; }
.flash-success { background:#064e3b; color:#6ee7b7; border:1px solid #065f46; }
.flash-error { background:#7f1d1d; color:#fca5a5; border:1px solid #991b1b; }
.result-box { background:#18181b; border-radius:12px; padding:2rem; border:2px solid {{ACCENT_COLOR}}; margin:2rem 0; }
.share-bar { display:flex; gap:0.8rem; margin-top:1rem; flex-wrap:wrap; }
.share-bar a { display:inline-flex; align-items:center; gap:0.4rem; padding:0.5rem 1rem; border-radius:8px; font-size:0.85rem; font-weight:600; text-decoration:none; transition:all 0.2s; }
.share-bar a:hover { text-decoration:none; transform:translateY(-1px); }
.share-twitter { background:#1d9bf0; color:#fff; }
.share-twitter:hover { background:#1a8cd8; }
@media (max-width:768px) {
.hero h1 { font-size:2rem; }
.hero p { font-size:1rem; }
}
</style>
"""
BMAC_FOOTER = """
<div style="text-align:center;padding:1.5rem;margin-top:2rem;border-top:1px solid rgba(255,255,255,0.08)">
<a href="https://buymeacoffee.com/r26xrthzttg" target="_blank" rel="noopener"
style="display:inline-flex;align-items:center;gap:0.5rem;background:linear-gradient(135deg,#FF813F,#FF5E0E);color:#fff;padding:0.6rem 1.4rem;border-radius:30px;text-decoration:none;font-weight:700;font-size:0.85rem;transition:all 0.2s;box-shadow:0 4px 15px rgba(255,94,14,0.25)"
onmouseover="this.style.transform='scale(1.05)';this.style.boxShadow='0 6px 20px rgba(255,94,14,0.4)'"
onmouseout="this.style.transform='scale(1)';this.style.boxShadow='0 4px 15px rgba(255,94,14,0.25)'">
<span style="font-size:1.1rem">☕</span> Support This App — Buy Me a Coffee
</a>
</div>
"""
BASE_LAYOUT = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta name="description" content="{{TAGLINE}}">
<title>{{page_title}} | {{APP_NAME}}</title>
{{STYLE}}
</head>
<body>
<nav>
<div class="container">
<a href="/" class="logo">🚀 {{APP_NAME}}</a>
<div class="links">
<a href="/">Home</a>
<a href="/pricing">Pricing</a>
{% if session.get('user_id') %}
<a href="/dashboard">Dashboard</a>
<a href="/logout" class="btn btn-outline">Logout</a>
{% else %}
<a href="/login" class="btn btn-outline">Login</a>
<a href="/register" class="btn btn-primary">Get Started</a>
{% endif %}
</div>
</div>
</nav>
<main class="container" style="flex:1">
{{CONTENT}}
</main>
{{BMAC_FOOTER}}
<footer>
<p>&copy; {{year}} {{APP_NAME}} — Built by Daily App Factory</p>
</footer>
</body>
</html>"""
# ─── ROUTES: Auth ─────────────────────────────────────────
@app.route('/register', methods=['GET', 'POST'])
def register():
error = None
if request.method == 'POST':
email = request.form.get('email', '').strip()
password = request.form.get('password', '').strip()
if not email or not password:
error = "All fields required"
elif len(password) < 4:
error = "Password too short"
else:
db = get_db()
exists = db.execute("SELECT id FROM users WHERE email=?", (email,)).fetchone()
if exists:
error = "Email already registered"
else:
db.execute("INSERT INTO users (email,password_hash) VALUES (?,?)",
(email, hash_password(password)))
db.commit()
user = db.execute("SELECT id FROM users WHERE email=?", (email,)).fetchone()
session['user_id'] = user['id']
session['email'] = email
db.close()
return redirect('/dashboard')
db.close()
content = f"""
<div style="max-width:400px;margin:3rem auto">
<h1 style="color:#fff;margin-bottom:1.5rem">Create Account</h1>
{'<div class="flash flash-error">'+error+'</div>' if error else ''}
<form method="POST">
<input name="email" type="email" placeholder="Email" required
style="width:100%;padding:0.8rem;background:#3f3f46;border:1px solid #52525b;border-radius:8px;color:#fff;margin-bottom:0.8rem;font-size:0.95rem">
<input name="password" type="password" placeholder="Password" required
style="width:100%;padding:0.8rem;background:#3f3f46;border:1px solid #52525b;border-radius:8px;color:#fff;margin-bottom:1rem;font-size:0.95rem">
<button type="submit" class="btn btn-primary" style="width:100%;justify-content:center">Create Account</button>
</form>
<p style="text-align:center;margin-top:1rem;color:#a1a1aa">Already have an account? <a href="/login">Login</a></p>
</div>"""
return BASE_LAYOUT.replace("{{STYLE}}", BASE_STYLE).replace("{{APP_NAME}}", APP_NAME).replace("{{TAGLINE}}", TAGLINE).replace("{{ACCENT_COLOR}}", ACCENT_COLOR).replace("{{PRIMARY_COLOR}}", PRIMARY_COLOR).replace("{{page_title}}", "Register").replace("{{CONTENT}}", content).replace("{{BMAC_FOOTER}}", BMAC_FOOTER).replace("{{year}}", str(datetime.now().year))
@app.route('/login', methods=['GET', 'POST'])
def login():
error = None
if request.method == 'POST':
email = request.form.get('email', '').strip()
password = request.form.get('password', '').strip()
db = get_db()
user = db.execute("SELECT * FROM users WHERE email=? AND password_hash=?",
(email, hash_password(password))).fetchone()
db.close()
if user:
session['user_id'] = user['id']
session['email'] = email
nxt = request.args.get('next', '/dashboard')
return redirect(nxt)
error = "Invalid email or password"
content = f"""
<div style="max-width:400px;margin:3rem auto">
<h1 style="color:#fff;margin-bottom:1.5rem">Welcome Back</h1>
{'<div class="flash flash-error">'+error+'</div>' if error else ''}
<form method="POST">
<input name="email" type="email" placeholder="Email" required
style="width:100%;padding:0.8rem;background:#3f3f46;border:1px solid #52525b;border-radius:8px;color:#fff;margin-bottom:0.8rem;font-size:0.95rem">
<input name="password" type="password" placeholder="Password" required
style="width:100%;padding:0.8rem;background:#3f3f46;border:1px solid #52525b;border-radius:8px;color:#fff;margin-bottom:1rem;font-size:0.95rem">
<button type="submit" class="btn btn-primary" style="width:100%;justify-content:center">Login</button>
</form>
<p style="text-align:center;margin-top:1rem;color:#a1a1aa">No account? <a href="/register">Create one</a></p>
</div>"""
return BASE_LAYOUT.replace("{{STYLE}}", BASE_STYLE).replace("{{APP_NAME}}", APP_NAME).replace("{{TAGLINE}}", TAGLINE).replace("{{ACCENT_COLOR}}", ACCENT_COLOR).replace("{{PRIMARY_COLOR}}", PRIMARY_COLOR).replace("{{page_title}}", "Login").replace("{{CONTENT}}", content).replace("{{BMAC_FOOTER}}", BMAC_FOOTER).replace("{{year}}", str(datetime.now().year))
@app.route('/logout')
def logout():
session.clear()
return redirect('/')
# ─── ROUTES: Landing ──────────────────────────────────────
@app.route('/')
def index():
content = f"""
<div class="hero">
<h1>{TAGLINE}</h1>
<p>{APP_NAME} helps you get results fast. Free to start, premium when you need more power. Built for people who want things done.</p>
<div style="display:flex;gap:1rem;justify-content:center;flex-wrap:wrap">
<a href="/register" class="btn btn-primary" style="font-size:1.1rem;padding:0.8rem 2rem">Try Free →</a>
<a href="/pricing" class="btn btn-outline" style="font-size:1.1rem;padding:0.8rem 2rem">View Pricing</a>
</div>
</div>
<div class="features">
<div class="feature-card">
<div class="icon">🎯</div>
<h3>Tailored to the Role</h3>
<p>Questions are matched to the real skills in the job description — not generic fluff.</p>
</div>
<div class="feature-card">
<div class="icon">🧠</div>
<h3>STAR Behavioral Set</h3>
<p>Proven behavioral questions with the STAR method baked in for strong 2-minute answers.</p>
</div>
<div class="feature-card">
<div class="icon">₿</div>
<h3>Pay with Bitcoin</h3>
<p>Premium upgrades via BTCPay Server. No credit card, no KYC.</p>
</div>
</div>
<div style="text-align:center;padding:2rem 0">
<h2 style="color:#fff;margin-bottom:0.5rem">Ready to get started?</h2>
<p style="color:#a1a1aa;margin-bottom:1.5rem">Join thousands of users who already trust {APP_NAME}</p>
<a href="/register" class="btn btn-primary" style="font-size:1.05rem">Create Free Account →</a>
</div>"""
return BASE_LAYOUT.replace("{{STYLE}}", BASE_STYLE).replace("{{APP_NAME}}", APP_NAME).replace("{{TAGLINE}}", TAGLINE).replace("{{ACCENT_COLOR}}", ACCENT_COLOR).replace("{{PRIMARY_COLOR}}", PRIMARY_COLOR).replace("{{page_title}}", "Home").replace("{{CONTENT}}", content).replace("{{BMAC_FOOTER}}", BMAC_FOOTER).replace("{{year}}", str(datetime.now().year))
# ─── ROUTES: Pricing ──────────────────────────────────────
@app.route('/pricing')
def pricing():
content = f"""
<div style="text-align:center;padding:3rem 0 2rem">
<h1 style="color:#fff;font-size:2.5rem;font-weight:800">Simple, Transparent Pricing</h1>
<p style="color:#a1a1aa;font-size:1.1rem;margin-top:0.5rem">Pay once, own forever. No subscriptions.</p>
</div>
<div class="pricing-grid">
<div class="pricing-card">
<h3>Free</h3>
<div class="price">$0</div>
<ul>
<li>Basic access</li>
<li>3 uses per day</li>
<li>Standard quality</li>
<li>Community support</li>
</ul>
<a href="/register" class="btn btn-outline" style="width:100%;justify-content:center">Get Started Free</a>
</div>
<div class="pricing-card premium">
<h3>Premium</h3>
<div class="price">${PREMIUM_PRICE}<span> one-time</span></div>
<ul>
<li>Unlimited access</li>
<li>Priority processing</li>
<li>Premium quality</li>
<li>Email support</li>
<li>Export & share features</li>
</ul>
<a href="/checkout" class="btn btn-primary" style="width:100%;justify-content:center">Upgrade Now</a>
</div>
</div>"""
return BASE_LAYOUT.replace("{{STYLE}}", BASE_STYLE).replace("{{APP_NAME}}", APP_NAME).replace("{{TAGLINE}}", TAGLINE).replace("{{ACCENT_COLOR}}", ACCENT_COLOR).replace("{{PRIMARY_COLOR}}", PRIMARY_COLOR).replace("{{page_title}}", "Pricing").replace("{{CONTENT}}", content).replace("{{BMAC_FOOTER}}", BMAC_FOOTER).replace("{{year}}", str(datetime.now().year))
# ─── ROUTES: Checkout & BTCPay ────────────────────────────
@app.route('/checkout')
@login_required
def checkout():
content = f"""
<div style="max-width:500px;margin:3rem auto;text-align:center">
<h1 style="color:#fff;margin-bottom:1rem">Upgrade to Premium</h1>
<p style="color:#a1a1aa;margin-bottom:2rem">One-time payment of <strong style="color:#fff">${PREMIUM_PRICE}</strong> via Bitcoin</p>
<div class="card" style="margin-bottom:1.5rem">
<h3 style="color:#fff;margin-bottom:0.5rem">What you get:</h3>
<ul style="list-style:none;text-align:left;color:#a1a1aa">
<li style="padding:0.3rem 0">✓ Unlimited access forever</li>
<li style="padding:0.3rem 0">✓ Premium features unlocked</li>
<li style="padding:0.3rem 0">✓ Priority support</li>
</ul>
</div>
<form method="POST" action="/checkout/create">
<input type="hidden" name="amount" value="{PREMIUM_PRICE}">
<button type="submit" class="btn btn-primary" style="width:100%;justify-content:center;font-size:1.1rem;padding:1rem">
₿ Pay ${PREMIUM_PRICE} with Bitcoin
</button>
</form>
<p style="margin-top:1rem;color:#71717a;font-size:0.85rem">Powered by BTCPay Server — secure, private, no middleman</p>
<p style="margin-top:0.5rem"><a href="/pricing">← Back to pricing</a></p>
</div>"""
return BASE_LAYOUT.replace("{{STYLE}}", BASE_STYLE).replace("{{APP_NAME}}", APP_NAME).replace("{{TAGLINE}}", TAGLINE).replace("{{ACCENT_COLOR}}", ACCENT_COLOR).replace("{{PRIMARY_COLOR}}", PRIMARY_COLOR).replace("{{page_title}}", "Checkout").replace("{{CONTENT}}", content).replace("{{BMAC_FOOTER}}", BMAC_FOOTER).replace("{{year}}", str(datetime.now().year))
@app.route('/checkout/create', methods=['POST'])
@login_required
def checkout_create():
amount = float(request.form.get('amount', PREMIUM_PRICE))
order_id = f"order_{session['user_id']}_{int(datetime.now().timestamp())}"
inv = create_btcpay_invoice(amount, order_id, f"{APP_NAME} Premium Upgrade")
if inv:
db = get_db()
db.execute("INSERT INTO payments (user_id,invoice_id,amount_usd) VALUES (?,?,?)",
(session['user_id'], inv['id'], amount))
db.execute("UPDATE users SET btcpay_invoice_id=? WHERE id=?",
(inv['id'], session['user_id']))
db.commit()
db.close()
return redirect(inv['checkout_url'])
content = """
<div style="text-align:center;padding:3rem">
<h2 style="color:#fca5a5">Payment Error</h2>
<p style="color:#a1a1aa;margin:1rem 0">Could not create payment invoice. Please try again.</p>
<a href="/checkout" class="btn btn-primary">Try Again</a>
</div>"""
return BASE_LAYOUT.replace("{{STYLE}}", BASE_STYLE).replace("{{APP_NAME}}", APP_NAME).replace("{{TAGLINE}}", TAGLINE).replace("{{ACCENT_COLOR}}", ACCENT_COLOR).replace("{{PRIMARY_COLOR}}", PRIMARY_COLOR).replace("{{page_title}}", "Error").replace("{{CONTENT}}", content).replace("{{BMAC_FOOTER}}", BMAC_FOOTER).replace("{{year}}", str(datetime.now().year))
@app.route('/webhook/btcpay', methods=['POST'])
def webhook_btcpay():
try:
data = request.get_json(force=True)
event_type = data.get('type', '')
invoice_id = data.get('invoiceId') or (data.get('data', {}).get('id'))
if event_type == 'InvoiceSettled' and invoice_id:
db = get_db()
db.execute("UPDATE payments SET status='settled',settled_at=datetime('now') WHERE invoice_id=?", (invoice_id,))
db.execute("UPDATE users SET is_premium=1,payment_status='paid' WHERE btcpay_invoice_id=?", (invoice_id,))
db.commit()
db.close()
return jsonify({"status": "ok"}), 200
except Exception as e:
app.logger.error(f"Webhook error: {e}")
return jsonify({"status": "error"}), 200
@app.route('/payment/success')
@login_required
def payment_success():
content = """
<div style="text-align:center;padding:4rem 0">
<div style="font-size:4rem;margin-bottom:1rem">✅</div>
<h1 style="color:#fff;margin-bottom:0.5rem">Payment Successful!</h1>
<p style="color:#a1a1aa;margin-bottom:2rem">Your premium features are now unlocked. Thank you!</p>
<a href="/dashboard" class="btn btn-primary">Go to Dashboard →</a>
</div>"""
return BASE_LAYOUT.replace("{{STYLE}}", BASE_STYLE).replace("{{APP_NAME}}", APP_NAME).replace("{{TAGLINE}}", TAGLINE).replace("{{ACCENT_COLOR}}", ACCENT_COLOR).replace("{{PRIMARY_COLOR}}", PRIMARY_COLOR).replace("{{page_title}}", "Payment Success").replace("{{CONTENT}}", content).replace("{{BMAC_FOOTER}}", BMAC_FOOTER).replace("{{year}}", str(datetime.now().year))
@app.route('/payment/cancel')
@login_required
def payment_cancel():
content = """
<div style="text-align:center;padding:4rem 0">
<h1 style="color:#fff;margin-bottom:0.5rem">Payment Cancelled</h1>
<p style="color:#a1a1aa;margin-bottom:2rem">No worries! You can upgrade anytime.</p>
<a href="/pricing" class="btn btn-primary">View Plans</a>
</div>"""
return BASE_LAYOUT.replace("{{STYLE}}", BASE_STYLE).replace("{{APP_NAME}}", APP_NAME).replace("{{TAGLINE}}", TAGLINE).replace("{{ACCENT_COLOR}}", ACCENT_COLOR).replace("{{PRIMARY_COLOR}}", PRIMARY_COLOR).replace("{{page_title}}", "Cancelled").replace("{{CONTENT}}", content).replace("{{BMAC_FOOTER}}", BMAC_FOOTER).replace("{{year}}", str(datetime.now().year))
# ─── ROUTES: Dashboard ────────────────────────────────────
@app.route('/dashboard')
@login_required
def dashboard():
db = get_db()
user = db.execute("SELECT * FROM users WHERE id=?", (session['user_id'],)).fetchone()
results = db.execute("SELECT * FROM results WHERE user_id=? ORDER BY created_at DESC LIMIT 10", (session['user_id'],)).fetchall()
db.close()
premium_badge = '<span style="background:' + ACCENT_COLOR + ';color:#fff;padding:0.2rem 0.8rem;border-radius:20px;font-size:0.8rem;font-weight:700">PREMIUM</span>' if user['is_premium'] else ''
upgrade_btn = '<a href="/checkout" class="btn btn-primary">Upgrade to Premium</a>' if not user['is_premium'] else ''
results_html = ''
for r in results:
results_html += '<div class="card" style="margin-bottom:0.8rem"><p style="color:#fff">' + str(r['data'])[:200] + '</p><small style="color:#71717a">' + r['created_at'] + '</small></div>'
empty_msg = '<p style="color:#a1a1aa">No results yet. Use the tool above to get started!</p>' if not results else ''
content = """<div style="padding:2rem 0">
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:1rem">
<h1 style="color:#fff">Dashboard """ + premium_badge + """</h1>
""" + upgrade_btn + """
</div>
<p style="color:#a1a1aa;margin-top:0.5rem">Welcome back, """ + user['email'] + """</p>
<div class="card" style="margin:2rem 0">
<h2 style="color:#fff;margin-bottom:1rem">Get Started</h2>
<p style="color:#a1a1aa;margin-bottom:1.5rem">Paste a job description and InterviewForge forges tailored technical, behavioral, and follow-up questions.</p>
<form method="POST" action="/process">
<input name="input_data" placeholder="Enter your text or data here..." required
style="width:100%;padding:0.8rem;background:#3f3f46;border:1px solid #52525b;border-radius:8px;color:#fff;margin-bottom:1rem;font-size:0.95rem">
<button type="submit" class="btn btn-primary">Process &rarr;</button>
</form>
</div>
<h2 style="color:#fff;margin:2rem 0 1rem">Recent Results</h2>
""" + empty_msg + """
""" + results_html + """
</div>"""
return BASE_LAYOUT.replace("{{STYLE}}", BASE_STYLE).replace("{{APP_NAME}}", APP_NAME).replace("{{TAGLINE}}", TAGLINE).replace("{{ACCENT_COLOR}}", ACCENT_COLOR).replace("{{PRIMARY_COLOR}}", PRIMARY_COLOR).replace("{{page_title}}", "Dashboard").replace("{{CONTENT}}", content).replace("{{BMAC_FOOTER}}", BMAC_FOOTER).replace("{{year}}", str(datetime.now().year))
# ─── CORE FEATURE ENGINE (stdlib only) ─────────────────────
SKILL_MAP = {
"python": ["Walk me through a Python project you built and what you would refactor.",
"How do you structure a large Python codebase for maintainability?",
"Explain the differences between a list, tuple, and set. When do you reach for each?"],
"java": ["Explain Java's inheritance model. How do you handle polymorphism in practice?",
"How would you make a concurrent Java application thread-safe?"],
"javascript":["How does the JavaScript event loop work?",
"Explain closures. When would you use one?"],
"sql": ["Explain the differences between INNER, LEFT, and FULL OUTER joins.",
"How do you optimize a slow query? Walk me through your process."],
"react": ["How does React handle re-rendering? How do you keep performance sane at scale?",
"Explain hooks and the rules of hooks."],
"aws": ["Walk me through how you would design a highly available service on AWS.",
"How do you manage costs and multi-account strategy in AWS?"],
"docker": ["Explain how Docker images, containers, and volumes relate.",
"How would you build a reproducible deployment pipeline with Docker?"],
"project management":["How do you prioritize when stakeholders have competing demands?",
"Tell me about a time a project was off track. What did you do?"],
"leadership": ["Give an example of a time you influenced a team without formal authority.",
"How do you handle an underperforming team member?"],
"sales": ["Walk me through your process for closing a difficult deal.",
"Tell me about a time you lost a deal. What would you change?"],
}
BEHAVIORAL = [
"Tell me about a time you faced a significant challenge at work. How did you handle it?",
"Describe a conflict with a coworker. How did you resolve it?",
"Give an example of a goal you met or missed, and what you learned.",
"How do you prioritize your tasks on a busy day?",
"Tell me about a time you made a mistake. What happened, and what changed?",
"Describe a situation where you had to make a quick decision with incomplete information.",
"Give an example of how you trained or mentored someone.",
"Tell me about a time you succeeded despite having minimal resources.",
]
CLARIFY = [
"What did you mean by that? Can you give a concrete example?",
"What was your specific role in that situation?",
"How did you measure the outcome? What were the numbers?",
"What would you do differently in hindsight?",
"Can you walk me through the decision process step by step?",
]
def detect_skills(text):
low = text.lower()
found = []
for skill, _ in SKILL_MAP.items():
if skill in low:
found.append(skill)
# generic tech markers
if not found:
for marker in ["software", "engineer", "developer", "devops", "data", "product", "design", "manager", "analyst"]:
if marker in low:
found.append("general")
break
return found if found else ["general"]
def build_questions(td):
import random
import re
role = re.sub(r'\s+', ' ', td.strip())
skills = detect_skills(role)
tech = []
for s in skills:
if s == "general":
tech += ["Walk me through your most complex technical project. What were the tradeoffs?",
"How do you debug a system that's failing in production but works locally?"]
else:
tech += SKILL_MAP[s]
random.seed(len(td)) # deterministic per input
behav = random.sample(BEHAVIORAL, k=min(3, len(BEHAVIORAL)))
clarify = random.sample(CLARIFY, k=2)
lines = []
lines.append("🎯 TAILORED INTERVIEW QUESTION SET")
lines.append("")
lines.append(f"Detected focus areas: {', '.join(skills).upper()}")
lines.append("")
lines.append("— TECHNICAL / ROLE-SPECIFIC —")
for i, q in enumerate(tech[:6], 1):
lines.append(f"{i}. {q}")
lines.append("")
lines.append("— BEHAVIORAL (STAR) —")
for i, q in enumerate(behav, 1):
lines.append(f"{i}. {q}")
lines.append("")
lines.append("— FOLLOW-UP / CLARIFYING —")
for q in clarify:
lines.append(f"{q}")
lines.append("")
lines.append("💡 STAR method: Situation → Task → Action → Result. Aim for 2-minute answers.")
return "\n".join(lines)
def _process_core(input_data):
return build_questions(input_data)
# ─── ROUTES: Core Feature (customize this!) ───────────────
@app.route('/process', methods=['POST'])
@login_required
def process():
input_data = request.form.get('input_data', '')
result = _process_core(input_data)
db = get_db()
db.execute("INSERT INTO results (user_id,data) VALUES (?,?)", (session['user_id'], result))
db.commit()
db.close()
tweet_text = f"I just used {APP_NAME}{TAGLINE} Check it out!"
tweet_url = f"https://twitter.com/intent/tweet?text={urllib.parse.quote(tweet_text)}"
content = f"""
<div style="padding:2rem 0">
<h1 style="color:#fff;margin-bottom:1rem">Your Result</h1>
<div class="result-box">
<p style="color:#e4e4e7;font-size:1.1rem;line-height:1.6;white-space:pre-wrap">{result}</p>
</div>
<div class="share-bar">
<a href="{tweet_url}" target="_blank" class="share-twitter">🐦 Share on X/Twitter</a>
</div>
<div style="margin-top:2rem">
<a href="/dashboard" class="btn btn-outline">← New Process</a>
</div>
</div>"""
return BASE_LAYOUT.replace("{{STYLE}}", BASE_STYLE).replace("{{APP_NAME}}", APP_NAME).replace("{{TAGLINE}}", TAGLINE).replace("{{ACCENT_COLOR}}", ACCENT_COLOR).replace("{{PRIMARY_COLOR}}", PRIMARY_COLOR).replace("{{page_title}}", "Result").replace("{{CONTENT}}", content).replace("{{BMAC_FOOTER}}", BMAC_FOOTER).replace("{{year}}", str(datetime.now().year))
# ─── ROUTES: SEO & Health ─────────────────────────────────
@app.route('/health')
def health():
return jsonify({"status": "ok", "app": APP_NAME, "slug": APP_SLUG, "version": "1.0.0"})
@app.route('/sitemap.xml')
def sitemap():
base = request.host_url.rstrip('/')
urls = ['/', '/pricing', '/register', '/login']
xml = '<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
for u in urls:
xml += f' <url><loc>{base}{u}</loc></url>\n'
xml += '</urlset>'
response = make_response(xml)
response.headers['Content-Type'] = 'application/xml'
return response
@app.route('/about')
def about():
content = f"""
<div style="max-width:700px;margin:3rem auto">
<h1 style="color:#fff;margin-bottom:1rem">About {APP_NAME}</h1>
<p style="color:#a1a1aa;line-height:1.7;margin-bottom:1rem">{TAGLINE}</p>
<p style="color:#a1a1aa;line-height:1.7;margin-bottom:1rem">Built with Flask, SQLite, and BTCPay Server for Bitcoin payments. Deployed on Proxmox infrastructure. Part of the Daily App Factory — one new app every day.</p>
<p style="color:#71717a;font-size:0.9rem">Version 1.0.0 — Built by Daily App Factory</p>
</div>"""
return BASE_LAYOUT.replace("{{STYLE}}", BASE_STYLE).replace("{{APP_NAME}}", APP_NAME).replace("{{TAGLINE}}", TAGLINE).replace("{{ACCENT_COLOR}}", ACCENT_COLOR).replace("{{PRIMARY_COLOR}}", PRIMARY_COLOR).replace("{{page_title}}", "About").replace("{{CONTENT}}", content).replace("{{BMAC_FOOTER}}", BMAC_FOOTER).replace("{{year}}", str(datetime.now().year))
# ─── ERROR HANDLERS ───────────────────────────────────────
@app.errorhandler(404)
def not_found(e):
content = """
<div style="text-align:center;padding:5rem 0">
<h1 style="color:#fff;font-size:4rem;margin-bottom:0.5rem">404</h1>
<p style="color:#a1a1aa;margin-bottom:2rem">Page not found</p>
<a href="/" class="btn btn-primary">Go Home</a>
</div>"""
return BASE_LAYOUT.replace("{{STYLE}}", BASE_STYLE).replace("{{APP_NAME}}", APP_NAME).replace("{{TAGLINE}}", TAGLINE).replace("{{ACCENT_COLOR}}", ACCENT_COLOR).replace("{{PRIMARY_COLOR}}", PRIMARY_COLOR).replace("{{page_title}}", "404").replace("{{CONTENT}}", content).replace("{{BMAC_FOOTER}}", BMAC_FOOTER).replace("{{year}}", str(datetime.now().year)), 404
@app.errorhandler(500)
def server_error(e):
content = """
<div style="text-align:center;padding:5rem 0">
<h1 style="color:#fff;font-size:4rem;margin-bottom:0.5rem">500</h1>
<p style="color:#a1a1aa;margin-bottom:2rem">Something went wrong. Please try again.</p>
<a href="/" class="btn btn-primary">Go Home</a>
</div>"""
return BASE_LAYOUT.replace("{{STYLE}}", BASE_STYLE).replace("{{APP_NAME}}", APP_NAME).replace("{{TAGLINE}}", TAGLINE).replace("{{ACCENT_COLOR}}", ACCENT_COLOR).replace("{{PRIMARY_COLOR}}", PRIMARY_COLOR).replace("{{page_title}}", "Error").replace("{{CONTENT}}", content).replace("{{BMAC_FOOTER}}", BMAC_FOOTER).replace("{{year}}", str(datetime.now().year)), 500
# ─── MAIN ─────────────────────────────────────────────────
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--port', type=int, default=5000)
parser.add_argument('--host', default='0.0.0.0')
args = parser.parse_args()
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
init_db()
print(f"🚀 {APP_NAME} running on {args.host}:{args.port}")
app.run(host=args.host, port=args.port, debug=False)

14
interviewforge.service Normal file
View File

@@ -0,0 +1,14 @@
[Unit]
Description=InterviewForge
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/opt/interviewforge
ExecStart=/usr/bin/python3 /opt/interviewforge/app.py
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target

10
nginx-interviewforge Normal file
View File

@@ -0,0 +1,10 @@
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;
}
}