From 8bb1ba4288d4f704312af6f4a2076e0d8dd32017 Mon Sep 17 00:00:00 2001 From: drjones Date: Tue, 18 Aug 2026 07:02:04 -0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20Initial=20release=20=E2=80=94=20?= =?UTF-8?q?Daily=20App=20Factory=20v7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LICENSE | 21 ++ README.md | 57 ++++ app.py | 695 +++++++++++++++++++++++++++++++++++++++++ interviewforge.service | 14 + nginx-interviewforge | 10 + 5 files changed, 797 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 app.py create mode 100644 interviewforge.service create mode 100644 nginx-interviewforge diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cab33dc --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b0d88f9 --- /dev/null +++ b/README.md @@ -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.* diff --git a/app.py b/app.py new file mode 100644 index 0000000..ac881f8 --- /dev/null +++ b/app.py @@ -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 = """ + +""" + +BMAC_FOOTER = """ +
+ + β˜• Support This App β€” Buy Me a Coffee + +
+""" + +BASE_LAYOUT = """ + + + + + + {{page_title}} | {{APP_NAME}} + {{STYLE}} + + + +
+ {{CONTENT}} +
+ {{BMAC_FOOTER}} + + +""" + +# ─── 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""" +
+

Create Account

+ {'
'+error+'
' if error else ''} +
+ + + +
+

Already have an account? Login

+
""" + 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""" +
+

Welcome Back

+ {'
'+error+'
' if error else ''} +
+ + + +
+

No account? Create one

+
""" + 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""" +
+

{TAGLINE}

+

{APP_NAME} helps you get results fast. Free to start, premium when you need more power. Built for people who want things done.

+
+ Try Free β†’ + View Pricing +
+
+
+
+
🎯
+

Tailored to the Role

+

Questions are matched to the real skills in the job description β€” not generic fluff.

+
+
+
🧠
+

STAR Behavioral Set

+

Proven behavioral questions with the STAR method baked in for strong 2-minute answers.

+
+
+
β‚Ώ
+

Pay with Bitcoin

+

Premium upgrades via BTCPay Server. No credit card, no KYC.

+
+
+
+

Ready to get started?

+

Join thousands of users who already trust {APP_NAME}

+ Create Free Account β†’ +
""" + 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""" +
+

Simple, Transparent Pricing

+

Pay once, own forever. No subscriptions.

+
+
+
+

Free

+
$0
+
    +
  • Basic access
  • +
  • 3 uses per day
  • +
  • Standard quality
  • +
  • Community support
  • +
+ Get Started Free +
+
+

Premium

+
${PREMIUM_PRICE} one-time
+
    +
  • Unlimited access
  • +
  • Priority processing
  • +
  • Premium quality
  • +
  • Email support
  • +
  • Export & share features
  • +
+ Upgrade Now +
+
""" + 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""" +
+

Upgrade to Premium

+

One-time payment of ${PREMIUM_PRICE} via Bitcoin

+
+

What you get:

+
    +
  • βœ“ Unlimited access forever
  • +
  • βœ“ Premium features unlocked
  • +
  • βœ“ Priority support
  • +
+
+
+ + +
+

Powered by BTCPay Server β€” secure, private, no middleman

+

← Back to pricing

+
""" + 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 = """ +
+

Payment Error

+

Could not create payment invoice. Please try again.

+ Try Again +
""" + 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 = """ +
+
βœ…
+

Payment Successful!

+

Your premium features are now unlocked. Thank you!

+ Go to Dashboard β†’ +
""" + 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 = """ +
+

Payment Cancelled

+

No worries! You can upgrade anytime.

+ View Plans +
""" + 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 = 'PREMIUM' if user['is_premium'] else '' + upgrade_btn = 'Upgrade to Premium' if not user['is_premium'] else '' + + results_html = '' + for r in results: + results_html += '

' + str(r['data'])[:200] + '

' + r['created_at'] + '
' + + empty_msg = '

No results yet. Use the tool above to get started!

' if not results else '' + + content = """
+
+

Dashboard """ + premium_badge + """

+ """ + upgrade_btn + """ +
+

Welcome back, """ + user['email'] + """

+
+

Get Started

+

Paste a job description and InterviewForge forges tailored technical, behavioral, and follow-up questions.

+
+ + +
+
+

Recent Results

+ """ + empty_msg + """ + """ + results_html + """ +
""" + 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""" +
+

Your Result

+
+

{result}

+
+ +
+ ← New Process +
+
""" + 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 = '\n\n' + for u in urls: + xml += f' {base}{u}\n' + xml += '' + response = make_response(xml) + response.headers['Content-Type'] = 'application/xml' + return response + +@app.route('/about') +def about(): + content = f""" +
+

About {APP_NAME}

+

{TAGLINE}

+

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.

+

Version 1.0.0 β€” Built by Daily App Factory

+
""" + 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 = """ +
+

404

+

Page not found

+ Go Home +
""" + 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 = """ +
+

500

+

Something went wrong. Please try again.

+ Go Home +
""" + 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) diff --git a/interviewforge.service b/interviewforge.service new file mode 100644 index 0000000..eeb3034 --- /dev/null +++ b/interviewforge.service @@ -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 diff --git a/nginx-interviewforge b/nginx-interviewforge new file mode 100644 index 0000000..11e8c96 --- /dev/null +++ b/nginx-interviewforge @@ -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; + } +}