- 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)
252 lines
8.5 KiB
Python
252 lines
8.5 KiB
Python
"""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)
|