🚀 Initial release — Daily App Factory v7
This commit is contained in:
21
LICENSE
Normal file
21
LICENSE
Normal 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
57
README.md
Normal 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
695
app.py
Normal 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>© {{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 →</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
14
interviewforge.service
Normal 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
10
nginx-interviewforge
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user