Files
justvitamin/app.py
Omair Saleh 3f2b6b6188 v3: PostgreSQL backend — 20K rows seeded, server-side SQL filtering
- Added PostgreSQL 16 Alpine service to Docker Swarm stack
- db.py: schema for 17 tables, auto-seed from jv_data.json on first boot
- /api/dashboard/data?start=&end= — server-side SQL filtering
  All Time: 0.43s (was 4MB JSON download stuck loading)
  Filtered (12mo): 0.20s with ~90% less data transferred
- Dashboard HTML patched: calls API instead of static JSON
- Integer casting for IsNewCustomer/HasDiscount/IsFreeShipping
- Advisory lock prevents race condition during parallel worker startup
- Returning Revenue now shows correctly: £14.5M (75% of total)
2026-03-02 20:24:15 +08:00

252 lines
8.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""JustVitamin × QuikCue — AI Content Engine
Live proposal site with real Gemini-powered demos.
PostgreSQL-backed data dashboard."""
import os, json, time, traceback, logging
from pathlib import Path
from flask import (Flask, render_template, jsonify, request,
send_from_directory, redirect)
from scraper import scrape_product, scrape_competitor
from ai_engine import (generate_asset_pack, competitor_xray, pdp_surgeon,
optimise_pdp_copy, generate_product_images)
from db import init_db, get_dashboard_data, get_meta
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")
app = Flask(__name__,
static_folder="static",
template_folder="templates")
GEN_DIR = Path(__file__).parent / "generated"
GEN_DIR.mkdir(exist_ok=True)
# ── Init DB on startup ───────────────────────────────────────
with app.app_context():
try:
init_db()
except Exception as e:
logging.error(f"DB init failed: {e}")
# ── Page routes ──────────────────────────────────────────────
@app.route("/")
def index():
return render_template("index.html")
@app.route("/dashboard")
@app.route("/dashboard/")
def dashboard():
return redirect("/dashboard/index.html")
@app.route("/dashboard/<path:filename>")
def dashboard_files(filename):
return send_from_directory("static/dashboard", filename)
# ── Dashboard API (Postgres-backed) ─────────────────────────
@app.route("/api/dashboard/data")
def api_dashboard_data():
"""Return filtered dashboard data from PostgreSQL.
Query params: ?start=YYYY-MM&end=YYYY-MM
Replaces the 4MB static JSON with fast server-side SQL filtering."""
start = request.args.get("start") or None
end = request.args.get("end") or None
try:
t0 = time.time()
data = get_dashboard_data(start, end)
data["_query_time"] = f"{time.time()-t0:.3f}s"
data["_source"] = "postgresql"
resp = jsonify(data)
resp.headers['Cache-Control'] = 'public, max-age=60'
return resp
except Exception as e:
traceback.print_exc()
return jsonify({"error": str(e)}), 500
@app.route("/api/dashboard/meta")
def api_dashboard_meta():
"""Return just the meta info (fast, tiny)."""
try:
return jsonify(get_meta())
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/proposal")
def proposal():
return send_from_directory("static/proposal", "index.html")
@app.route("/offer")
def offer():
return send_from_directory("static/offer", "index.html")
@app.route("/generated/<path:filename>")
def serve_generated(filename):
return send_from_directory(GEN_DIR, filename)
# ── API: Scrape ──────────────────────────────────────────────
@app.route("/api/scrape", methods=["POST"])
def api_scrape():
"""Scrape a JustVitamins product page."""
url = request.json.get("url", "").strip()
if not url:
return jsonify({"error": "No URL provided"}), 400
try:
data = scrape_product(url)
if not data.get("title"):
return jsonify({"error": "Could not find product. Check URL."}), 400
return jsonify(data)
except Exception as e:
traceback.print_exc()
return jsonify({"error": str(e)}), 500
@app.route("/api/scrape-competitor", methods=["POST"])
def api_scrape_competitor():
"""Scrape any competitor product page."""
url = request.json.get("url", "").strip()
if not url:
return jsonify({"error": "No URL provided"}), 400
try:
data = scrape_competitor(url)
if not data.get("title"):
return jsonify({"error": "Could not extract product data."}), 400
return jsonify(data)
except Exception as e:
traceback.print_exc()
return jsonify({"error": str(e)}), 500
# ── API: Demo A — 12-Asset Pack ──────────────────────────────
@app.route("/api/generate-pack", methods=["POST"])
def api_generate_pack():
"""Generate a full 12-asset marketing pack from product data."""
product = request.json
if not product:
return jsonify({"error": "No product data"}), 400
try:
t0 = time.time()
pack = generate_asset_pack(product)
pack["_generation_time"] = f"{time.time()-t0:.1f}s"
pack["_product_title"] = product.get("title", "")
return jsonify(pack)
except Exception as e:
traceback.print_exc()
return jsonify({"error": str(e)}), 500
# ── API: Demo B — Competitor X-Ray ───────────────────────────
@app.route("/api/competitor-xray", methods=["POST"])
def api_competitor_xray():
"""Scrape competitor + run AI analysis."""
url = request.json.get("url", "").strip()
if not url:
return jsonify({"error": "No URL provided"}), 400
try:
t0 = time.time()
comp_data = scrape_competitor(url)
if not comp_data.get("title"):
return jsonify({"error": "Could not scrape competitor page."}), 400
analysis = competitor_xray(comp_data)
analysis["_scrape_data"] = {
"title": comp_data.get("title"),
"price": comp_data.get("price"),
"brand": comp_data.get("brand"),
"url": url,
}
analysis["_generation_time"] = f"{time.time()-t0:.1f}s"
return jsonify(analysis)
except Exception as e:
traceback.print_exc()
return jsonify({"error": str(e)}), 500
# ── API: Demo C — PDP Surgeon ────────────────────────────────
@app.route("/api/pdp-surgeon", methods=["POST"])
def api_pdp_surgeon():
"""Rewrite PDP copy in a given style."""
data = request.json or {}
product = data.get("product")
style = data.get("style", "balanced")
if not product:
return jsonify({"error": "No product data"}), 400
try:
t0 = time.time()
result = pdp_surgeon(product, style)
result["_generation_time"] = f"{time.time()-t0:.1f}s"
return jsonify(result)
except Exception as e:
traceback.print_exc()
return jsonify({"error": str(e)}), 500
# ── API: Full PDP Optimise ───────────────────────────────────
@app.route("/api/optimise", methods=["POST"])
def api_optimise():
"""Full PDP rewrite — scrape + AI copy."""
product = request.json
if not product:
return jsonify({"error": "No product data"}), 400
try:
t0 = time.time()
copy = optimise_pdp_copy(product)
copy["_generation_time"] = f"{time.time()-t0:.1f}s"
return jsonify(copy)
except Exception as e:
traceback.print_exc()
return jsonify({"error": str(e)}), 500
# ── API: Image Generation ────────────────────────────────────
@app.route("/api/generate-images", methods=["POST"])
def api_generate_images():
"""Generate AI product images via Nano Banana / Pro."""
product = request.json
if not product:
return jsonify({"error": "No product data"}), 400
try:
t0 = time.time()
images = generate_product_images(product)
images["_generation_time"] = f"{time.time()-t0:.1f}s"
return jsonify(images)
except Exception as e:
traceback.print_exc()
return jsonify({"error": str(e)}), 500
# ── Health check ─────────────────────────────────────────────
@app.route("/api/health")
def health():
db_ok = False
try:
from db import get_conn
conn = get_conn()
cur = conn.cursor()
cur.execute("SELECT COUNT(*) FROM monthly")
count = cur.fetchone()[0]
cur.close()
conn.close()
db_ok = count > 0
except Exception:
pass
return jsonify({
"status": "ok",
"gemini_key_set": bool(os.environ.get("GEMINI_API_KEY")),
"db_connected": db_ok,
"db_rows": count if db_ok else 0,
})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5050, debug=True)